From 5b28e59ca77c7eb33ea48c1606afb25ffb108fb0 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:55:50 +0530 Subject: [PATCH] Navbar drag drop using dnd kit (#18288) --- packages/twenty-front/jest.config.mjs | 4 +- packages/twenty-front/package.json | 1 + .../constants/DefaultRecordActionsConfig.tsx | 18 +- ...vigationSidebarNoSelectionRecordAction.tsx | 16 + .../constants/RecordAgnosticActionsConfig.tsx | 27 +- .../hooks/useRecordAgnosticActions.ts | 4 + .../types/RecordAgnosticActionsKeys.ts | 1 + .../useShouldActionBeRegisteredParams.ts | 4 +- .../CommandMenuAddToNavDroppableDndKit.tsx | 27 ++ .../CommandMenuAddToNavDroppableTypes.ts | 7 + ...dMenuItemWithAddToNavigationDragDndKit.tsx | 27 ++ .../CurrentWorkspaceMemberFavorites.tsx | 11 +- ...CurrentWorkspaceMemberFavoritesFolders.tsx | 21 +- .../CurrentWorkspaceMemberOrphanFavorites.tsx | 5 +- .../components/FavoritesFolderContent.tsx | 5 +- .../favorites/components/FavoritesFolders.tsx | 14 +- .../hooks/useHandleFavoriteDragAndDrop.ts | 4 +- .../components/AddToNavigationDragHandle.tsx | 21 +- ...rkspaceMemberNavigationMenuItemFolders.tsx | 20 +- ...berNavigationMenuItemFoldersDispatcher.tsx | 25 +- ...rentWorkspaceMemberNavigationMenuItems.tsx | 7 +- ...rkspaceMemberOrphanNavigationMenuItems.tsx | 7 +- .../components/LinkIconWithLinkOverlay.tsx | 122 +++++ .../components/NavigationItemDropTarget.tsx | 44 +- .../components/NavigationMenuEditModeBar.tsx | 51 ++- .../NavigationMenuItemDroppable.tsx | 53 +-- .../NavigationMenuItemFolderContent.tsx | 7 +- .../components/NavigationMenuItemFolders.tsx | 12 +- .../components/NavigationMenuItemIcon.tsx | 14 +- .../components/ObjectIconWithViewOverlay.tsx | 12 +- .../WorkspaceDndKitDroppableSlot.tsx | 51 +++ .../WorkspaceDndKitSortableItem.tsx | 59 +++ ...rkspaceNavigationMenuItemFolderSubItem.tsx | 101 +++++ .../WorkspaceNavigationMenuItems.tsx | 28 +- .../WorkspaceNavigationMenuItemsFolder.tsx | 428 ++++++++---------- .../constants/DndKitDropTargetIdSeparator.ts | 1 + .../contexts/SortableDropTargetRefContext.tsx | 5 + .../hooks/useHandleAddToNavigationDrop.ts | 13 +- ...eWorkspaceNavigationMenuItemDragAndDrop.ts | 81 ++-- .../hooks/useNavigationMenuItemMoveRemove.ts | 89 ++-- .../hooks/useOpenAddItemToFolderPage.ts | 7 +- .../useOpenNavigationMenuItemInSidePanel.ts | 11 + .../hooks/useSaveNavigationMenuItemsDraft.ts | 70 +-- .../hooks/useWorkspaceFolderOpenState.ts | 72 +++ .../hooks/useWorkspaceSectionItems.ts | 11 +- .../types/AddMenuItemInsertionContext.ts | 1 + .../__tests__/extractDomainFromUrl.test.ts | 15 + .../__tests__/getPositionBetween.test.ts | 27 ++ .../__tests__/orderFoldersForCreation.test.ts | 35 ++ .../buildCreateNavigationMenuItemInput.ts | 48 ++ .../utils/extractDomainFromUrl.ts | 8 + .../utils/getDndKitDropTargetId.ts | 6 + ...getFavoritesDropTargetIdFromDestination.ts | 22 + .../utils/getLinkFaviconUrl.ts | 20 + .../utils/getPositionBetween.ts | 18 + .../utils/orderFoldersForCreation.ts | 24 + .../FavoritesDragDropProviderContent.tsx | 177 ++++++++ .../MainNavigationDrawerScrollableItems.tsx | 8 +- .../NavigationDrawerOtherSection.tsx | 38 +- .../components/PageDragDropProvider.tsx | 185 +------- .../PageDragDropProviderMountEffect.tsx | 24 + .../components/WorkspaceDndKitProvider.tsx | 50 ++ .../workspaceDndKitDropResultOptions.ts | 6 + .../navigation/hooks/useWorkspaceDndKit.ts | 351 ++++++++++++++ .../navigation/preloadWorkspaceDndKit.ts | 16 + .../types/workspaceDndKitDraggableData.ts | 4 + .../types/workspaceDndKitDropDestination.ts | 1 + .../types/workspaceDndKitDroppableData.ts | 4 + ...orkspaceDndKitSortableTargetDestination.ts | 8 + ...eDndKitGetDestinationFromSortableTarget.ts | 53 +++ .../utils/workspaceDndKitIsFolderDrag.ts | 13 + .../utils/workspaceDndKitResolveDropTarget.ts | 50 ++ .../utils/workspaceDndKitToDropResult.ts | 20 + .../components/EditModeProps.ts | 4 + ...igationDrawerItemForObjectMetadataItem.tsx | 25 +- .../NavigationDrawerOpenedSection.tsx | 22 +- ...ionDrawerSectionForObjectMetadataItems.tsx | 4 +- ...onDrawerSectionForWorkspaceItemContent.tsx | 64 +++ ...erSectionForWorkspaceItemFolderContent.tsx | 72 +++ ...awerSectionForWorkspaceItemLinkContent.tsx | 45 ++ ...erSectionForWorkspaceItemObjectContent.tsx | 44 ++ ...vigationDrawerSectionForWorkspaceItems.tsx | 272 +++-------- ...awerSectionForWorkspaceItemsListDndKit.tsx | 109 +++++ ...erSectionForWorkspaceItemsListReadOnly.tsx | 51 +++ ...onDrawerWorkspaceSectionSkeletonLoader.tsx | 53 +++ .../components/WorkspaceFolderReadOnly.tsx | 120 +++++ .../components/WorkspaceOrphanDropTarget.tsx | 31 ++ .../WorkspaceSectionAddMenuItemButton.tsx | 55 +++ .../WorkspaceSectionItemContentProps.ts | 23 + .../WorkspaceSectionListDndKitProps.ts | 20 + .../WorkspaceSectionListEditModeFallback.tsx | 32 ++ .../read-only/hooks/useIsRecordReadOnly.ts | 19 +- .../RecordTableEmptyStateDisplay.tsx | 15 +- ...bleHeaderLabelIdentifierCellPlusButton.tsx | 15 +- .../SidePanelAddToNavigationDroppable.tsx | 31 +- .../SidePanelItemWithAddToNavigationDrag.tsx | 90 ++-- .../components/SidePanelLinkInfo.tsx | 16 +- .../side-panel/components/SidePanelRouter.tsx | 17 +- .../components/SidePanelEditLinkItemView.tsx | 55 ++- .../SidePanelNavigationMenuItemEditPage.tsx | 24 +- .../SidePanelNewSidebarItemMainMenu.tsx | 166 ++++--- .../SidePanelNewSidebarItemObjectFlow.tsx | 31 +- .../SidePanelNewSidebarItemPage.tsx | 14 +- .../SidePanelNewSidebarItemRecordItem.tsx | 31 +- .../SidePanelNewSidebarItemRecordSubView.tsx | 22 +- .../SidePanelNewSidebarItemViewFlow.tsx | 14 +- ...dePanelNewSidebarItemViewPickerSubView.tsx | 36 +- .../components/SidePanelObjectMenuItem.tsx | 6 +- .../components/SidePanelObjectPickerItem.tsx | 5 +- .../SidePanelObjectPickerSubView.tsx | 48 +- .../SidePanelSystemObjectPickerSubView.tsx | 72 ++- .../hooks/useAddFolderToNavigationMenu.ts | 6 +- .../hooks/useAddLinkToNavigationMenu.ts | 6 +- ...seNavigationMenuItemEditOrganizeActions.ts | 1 + ...vailableObjectMetadataForNewSidebarItem.ts | 6 +- .../components/NavigationDrawerItem.tsx | 29 +- .../NavigationDrawerSectionTitle.tsx | 33 +- .../components/NavigationDrawerSubItem.tsx | 2 + yarn.lock | 154 +++++-- 119 files changed, 3563 insertions(+), 1297 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableDndKit.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableTypes.ts create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitDroppableSlot.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitSortableItem.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/constants/DndKitDropTargetIdSeparator.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/contexts/SortableDropTargetRefContext.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceFolderOpenState.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/extractDomainFromUrl.test.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/getPositionBetween.test.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/orderFoldersForCreation.test.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/buildCreateNavigationMenuItemInput.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/extractDomainFromUrl.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/getDndKitDropTargetId.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/getPositionBetween.ts create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/orderFoldersForCreation.ts create mode 100644 packages/twenty-front/src/modules/navigation/components/FavoritesDragDropProviderContent.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx create mode 100644 packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx create mode 100644 packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts create mode 100644 packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts create mode 100644 packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts create mode 100644 packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts create mode 100644 packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts create mode 100644 packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts create mode 100644 packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts create mode 100644 packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts create mode 100644 packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts create mode 100644 packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts create mode 100644 packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/components/EditModeProps.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceFolderReadOnly.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceOrphanDropTarget.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionAddMenuItemButton.tsx create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionItemContentProps.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListDndKitProps.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx diff --git a/packages/twenty-front/jest.config.mjs b/packages/twenty-front/jest.config.mjs index 357f5f9229..6b964fe6f3 100644 --- a/packages/twenty-front/jest.config.mjs +++ b/packages/twenty-front/jest.config.mjs @@ -61,8 +61,8 @@ const jestConfig = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 49.5, - lines: 48, + statements: 49.3, + lines: 47.9, functions: 39.5, }, }, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 0c85a856ed..9415d605b6 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -38,6 +38,7 @@ "@calcom/embed-react": "^1.5.3", "@cyntler/react-doc-viewer": "^1.17.0", "@dagrejs/dagre": "^1.1.8", + "@dnd-kit/react": "^0.3.2", "@floating-ui/react": "^0.24.3", "@graphiql/plugin-explorer": "^1.0.2", "@graphiql/react": "^0.23.0", diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx index 36988749fe..0501036f77 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx @@ -29,19 +29,19 @@ import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/sin import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig'; import { ActionScope } from '@/action-menu/actions/types/ActionScope'; import { ActionType } from '@/action-menu/actions/types/ActionType'; -import { - ActionViewType, - CoreObjectNameSingular, - AppPath, - SettingsPath, -} from 'twenty-shared/types'; import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; +import { msg } from '@lingui/core/macro'; +import { isNonEmptyString } from '@sniptt/guards'; import { BACKEND_BATCH_REQUEST_MAX_COUNT, MUTATION_MAX_MERGE_RECORDS, } from 'twenty-shared/constants'; -import { msg } from '@lingui/core/macro'; -import { isNonEmptyString } from '@sniptt/guards'; +import { + ActionViewType, + AppPath, + CoreObjectNameSingular, + SettingsPath, +} from 'twenty-shared/types'; import { IconArrowMerge, IconBuildingSkyscraper, @@ -72,8 +72,8 @@ import { import { isDefined } from 'twenty-shared/utils'; import { - PermissionFlagType, FeatureFlagKey, + PermissionFlagType, } from '~/generated-metadata/graphql'; export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction.tsx new file mode 100644 index 0000000000..d7eba36271 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction.tsx @@ -0,0 +1,16 @@ +import { Action } from '@/action-menu/actions/components/Action'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; + +export const EditNavigationSidebarNoSelectionRecordAction = () => { + const setIsNavigationMenuInEditMode = useSetAtomState( + isNavigationMenuInEditModeState, + ); + + return ( + setIsNavigationMenuInEditMode(true)} + closeSidePanelOnCommandMenuListActionExecution + /> + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig.tsx index 55740e16f7..796b5a704d 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig.tsx @@ -1,11 +1,18 @@ import { ActionOpenSidePanelPage } from '@/action-menu/actions/components/ActionOpenSidePanelPage'; +import { EditNavigationSidebarNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction'; import { RecordAgnosticActionsKeys } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys'; import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig'; import { ActionScope } from '@/action-menu/actions/types/ActionScope'; import { ActionType } from '@/action-menu/actions/types/ActionType'; -import { ActionViewType, SidePanelPages } from 'twenty-shared/types'; import { msg } from '@lingui/core/macro'; -import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display'; +import { ActionViewType, SidePanelPages } from 'twenty-shared/types'; +import { + IconHistory, + IconLayout, + IconSearch, + IconSparkles, +} from 'twenty-ui/display'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record = { [RecordAgnosticActionsKeys.SEARCH_RECORDS]: { @@ -88,4 +95,20 @@ export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record = { ), shouldBeRegistered: () => true, }, + [RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR]: { + type: ActionType.Navigation, + scope: ActionScope.Global, + key: RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR, + label: msg`Edit navigation sidebar`, + shortLabel: msg`Edit sidebar`, + position: 4, + Icon: IconLayout, + isPinned: false, + availableOn: [ActionViewType.GLOBAL], + shouldBeRegistered: ({ isFeatureFlagEnabled }) => + isFeatureFlagEnabled( + FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, + ), + component: , + }, }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions.ts index 8c59c27692..caa3c42359 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions.ts @@ -14,6 +14,10 @@ export const useRecordAgnosticActions = () => { RECORD_AGNOSTIC_ACTIONS_CONFIG[ RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK ], + [RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR]: + RECORD_AGNOSTIC_ACTIONS_CONFIG[ + RecordAgnosticActionsKeys.EDIT_NAVIGATION_SIDEBAR + ], }; if (isAiEnabled) { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys.ts b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys.ts index 4ddfe046cc..f6881f71a8 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys.ts @@ -3,4 +3,5 @@ export enum RecordAgnosticActionsKeys { SEARCH_RECORDS_FALLBACK = 'search-records-fallback', ASK_AI = 'ask-ai', VIEW_PREVIOUS_AI_CHATS = 'view-previous-ai-chats', + EDIT_NAVIGATION_SIDEBAR = 'edit-navigation-sidebar', } diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts index 2e4d5f9541..41012f1805 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts @@ -11,19 +11,19 @@ import { useFavorites } from '@/favorites/hooks/useFavorites'; import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData'; import { CoreObjectNameSingular } from 'twenty-shared/types'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; +import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useStore } from 'jotai'; import { useCallback, useContext, useMemo } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; -import { useStore } from 'jotai'; export const useShouldActionBeRegisteredParams = ({ objectMetadataItem, diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableDndKit.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableDndKit.tsx new file mode 100644 index 0000000000..66a0a6e52b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableDndKit.tsx @@ -0,0 +1,27 @@ +import { useDroppable } from '@dnd-kit/react'; +import { type ReactNode } from 'react'; + +import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; + +import type { AddToNavDroppableProvided } from '@/command-menu/components/CommandMenuAddToNavDroppableTypes'; + +type CommandMenuAddToNavDroppableDndKitProps = { + children: (provided: AddToNavDroppableProvided) => ReactNode; + isDropDisabled: boolean; +}; + +export const CommandMenuAddToNavDroppableDndKit = ({ + children, + isDropDisabled, +}: CommandMenuAddToNavDroppableDndKitProps) => { + const { ref } = useDroppable({ + id: ADD_TO_NAV_SOURCE_DROPPABLE_ID, + disabled: isDropDisabled, + }); + const provided: AddToNavDroppableProvided = { + innerRef: ref, + droppableProps: { 'data-dnd-group': ADD_TO_NAV_SOURCE_DROPPABLE_ID }, + placeholder: null, + }; + return <>{children(provided)}; +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableTypes.ts b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableTypes.ts new file mode 100644 index 0000000000..0ae6da7d0d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuAddToNavDroppableTypes.ts @@ -0,0 +1,7 @@ +import type { ReactNode } from 'react'; + +export type AddToNavDroppableProvided = { + innerRef: (element: HTMLElement | null) => void; + droppableProps: object; + placeholder: ReactNode; +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit.tsx new file mode 100644 index 0000000000..dc3b408c9a --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit.tsx @@ -0,0 +1,27 @@ +import { useDraggable } from '@dnd-kit/react'; +import { type ReactNode } from 'react'; + +import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; + +type CommandMenuItemWithAddToNavigationDragDndKitProps = { + id: string; + dragIndex: number; + menuItemContent: ReactNode; +}; + +export const CommandMenuItemWithAddToNavigationDragDndKit = ({ + id, + dragIndex, + menuItemContent, +}: CommandMenuItemWithAddToNavigationDragDndKitProps) => { + const { ref } = useDraggable({ + id, + data: { + sourceDroppableId: ADD_TO_NAV_SOURCE_DROPPABLE_ID, + sourceIndex: dragIndex, + }, + disabled: false, + feedback: 'clone', + }); + return
{menuItemContent}
; +}; diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx index b4dac03512..d0d1f3b686 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx @@ -7,12 +7,10 @@ import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder'; import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder'; import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel'; import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite'; import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; @@ -24,9 +22,11 @@ import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/componen import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; 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 { Droppable } from '@hello-pangea/dnd'; import { useLingui } from '@lingui/react/macro'; import { useContext, useState } from 'react'; @@ -239,7 +239,10 @@ export const CurrentWorkspaceMemberFavorites = ({ rightOptions={ deleteFavorite(favorite.id)} + onClick={(e) => { + e.stopPropagation(); + deleteFavorite(favorite.id); + }} accent="tertiary" /> } diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavoritesFolders.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavoritesFolders.tsx index 7dd32ceb01..2476039371 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavoritesFolders.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavoritesFolders.tsx @@ -1,11 +1,11 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites'; +import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider'; import { FavoriteFolders } from '@/favorites/components/FavoritesFolders'; import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder'; import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; @@ -13,11 +13,13 @@ import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/ 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'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useLingui } from '@lingui/react/macro'; import { isDefined } from 'twenty-shared/utils'; import { IconFolderPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; export const CurrentWorkspaceMemberFavoritesFolders = () => { const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); @@ -69,14 +71,21 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => { accent="tertiary" /> } + isOpen={isNavigationSectionOpen} /> - {isNavigationSectionOpen && ( - <> - + + + - - )} + + ); }; diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx index 994cb935c9..bdb0fbde39 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx @@ -63,7 +63,10 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => { rightOptions={ deleteFavorite(favorite.id)} + onClick={(e) => { + e.stopPropagation(); + deleteFavorite(favorite.id); + }} accent="tertiary" /> } diff --git a/packages/twenty-front/src/modules/favorites/components/FavoritesFolderContent.tsx b/packages/twenty-front/src/modules/favorites/components/FavoritesFolderContent.tsx index 1e3e9b01f2..e262a57cb8 100644 --- a/packages/twenty-front/src/modules/favorites/components/FavoritesFolderContent.tsx +++ b/packages/twenty-front/src/modules/favorites/components/FavoritesFolderContent.tsx @@ -54,7 +54,10 @@ export const FavoritesFolderContent = ({ rightOptions={ deleteFavorite(favorite.id)} + onClick={(e) => { + e.stopPropagation(); + deleteFavorite(favorite.id); + }} accent="tertiary" /> } diff --git a/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx b/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx index e2538ad3a0..84701b4444 100644 --- a/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx +++ b/packages/twenty-front/src/modules/favorites/components/FavoritesFolders.tsx @@ -2,19 +2,13 @@ import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentW import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder'; import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder'; import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState'; -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'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useState } from 'react'; import { IconFolder } from 'twenty-ui/display'; -type FavoriteFoldersProps = { - isNavigationSectionOpen: boolean; -}; - -export const FavoriteFolders = ({ - isNavigationSectionOpen, -}: FavoriteFoldersProps) => { +export const FavoriteFolders = () => { const [newFolderName, setNewFolderName] = useState(''); const { favoritesByFolder } = useFavoritesByFolder(); @@ -56,10 +50,6 @@ export const FavoriteFolders = ({ setIsFavoriteFolderCreating(false); }; - if (!isNavigationSectionOpen) { - return null; - } - return ( <> {isFavoriteFolderCreating && ( diff --git a/packages/twenty-front/src/modules/favorites/hooks/useHandleFavoriteDragAndDrop.ts b/packages/twenty-front/src/modules/favorites/hooks/useHandleFavoriteDragAndDrop.ts index 49a9a6ef91..ad557ab994 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/useHandleFavoriteDragAndDrop.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/useHandleFavoriteDragAndDrop.ts @@ -1,12 +1,12 @@ import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds'; import { useSortedFavorites } from '@/favorites/hooks/useSortedFavorites'; import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition'; import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { type OnDragEndResponder } from '@hello-pangea/dnd'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData'; export const useHandleFavoriteDragAndDrop = () => { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/AddToNavigationDragHandle.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/AddToNavigationDragHandle.tsx index d5bace190a..5f23d979c3 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/AddToNavigationDragHandle.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/AddToNavigationDragHandle.tsx @@ -1,6 +1,6 @@ import { styled } from '@linaria/react'; import { isNonEmptyString } from '@sniptt/guards'; -import { type ReactNode, useContext } from 'react'; +import { useContext, type ReactNode } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { IconGripVertical, type IconComponent } from 'twenty-ui/display'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; @@ -11,19 +11,26 @@ import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/ import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload'; -const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>` +const StyledIconSlot = styled.div<{ + $hasFixedSize: boolean; + $disabled?: boolean; + $disableDrag?: boolean; +}>` align-items: center; - cursor: grab; + cursor: ${({ $disabled, $disableDrag }) => + $disabled || $disableDrag ? 'default' : 'grab'}; display: flex; flex-shrink: 0; height: ${({ $hasFixedSize }) => $hasFixedSize ? themeCssVariables.spacing[4] : 'auto'}; justify-content: center; + opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)}; width: ${({ $hasFixedSize }) => $hasFixedSize ? themeCssVariables.spacing[4] : 'auto'}; &:active { - cursor: grabbing; + cursor: ${({ $disabled, $disableDrag }) => + $disabled || $disableDrag ? 'default' : 'grabbing'}; } `; @@ -63,6 +70,8 @@ type AddToNavigationDragHandleProps = { customIconContent?: ReactNode; payload: AddToNavigationDragPayload; isHovered: boolean; + disabled?: boolean; + disableDrag?: boolean; }; export const AddToNavigationDragHandle = ({ @@ -70,6 +79,8 @@ export const AddToNavigationDragHandle = ({ customIconContent, payload, isHovered, + disabled = false, + disableDrag = false, }: AddToNavigationDragHandleProps) => { const { theme } = useContext(ThemeContext); const effectiveColor = @@ -89,6 +100,8 @@ export const AddToNavigationDragHandle = ({ return ( {isHovered ? ( { accent="tertiary" /> } + isOpen={isNavigationSectionOpen} /> - {isNavigationSectionOpen && ( - <> - - - - )} + + + + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher.tsx index b26a1f36a2..dbe1f8c608 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher.tsx @@ -2,16 +2,31 @@ import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders'; import { CurrentWorkspaceMemberNavigationMenuItemFolders } from '@/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFolders'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { FavoritesDragDropProviderContent } from '@/navigation/components/FavoritesDragDropProviderContent'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { AnimatedEaseInOut } from 'twenty-ui/utilities'; export const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = () => { const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, ); + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); - if (isNavigationMenuItemEditingEnabled) { - return ; - } - - return ; + return ( + + {isNavigationMenuItemEditingEnabled ? ( + + + + ) : ( + + + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx index 57b4e57224..22563dc233 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx @@ -291,9 +291,10 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({ isWorkspaceFolder ? undefined : ( - deleteNavigationMenuItem(navigationMenuItem.id) - } + onClick={(e) => { + e.stopPropagation(); + deleteNavigationMenuItem(navigationMenuItem.id); + }} accent="tertiary" /> ) diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems.tsx index b487230212..012bac477d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems.tsx @@ -85,9 +85,10 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => { rightOptions={ - deleteNavigationMenuItem(navigationMenuItem.id) - } + onClick={(e) => { + e.stopPropagation(); + deleteNavigationMenuItem(navigationMenuItem.id); + }} accent="tertiary" /> } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx new file mode 100644 index 0000000000..4e93748e0c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx @@ -0,0 +1,122 @@ +import { styled } from '@linaria/react'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import type { IconComponent } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink'; +import { getLinkFaviconUrl } from '@/navigation-menu-item/utils/getLinkFaviconUrl'; +import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor'; + +const failedFaviconUrls = new Set(); + +const StyledCompositeContainer = styled.div` + align-items: center; + border-radius: 4px; + box-sizing: border-box; + display: flex; + flex-shrink: 0; + height: 16px; + justify-content: center; + position: relative; + width: 16px; +`; + +const StyledMainIconWrapper = styled.div<{ + $backgroundColor: string; + $borderColor?: string; + $noBackgroundOrBorder?: boolean; +}>` + align-items: center; + background-color: ${({ $backgroundColor, $noBackgroundOrBorder }) => + $noBackgroundOrBorder ? 'transparent' : $backgroundColor}; + border: ${({ $borderColor, $noBackgroundOrBorder }) => + $noBackgroundOrBorder || !$borderColor + ? 'none' + : `1px solid ${$borderColor}`}; + border-radius: 4px; + box-sizing: border-box; + display: flex; + inset: 0; + justify-content: center; + overflow: hidden; + position: absolute; +`; + +const StyledFaviconImage = styled.img` + height: 100%; + object-fit: contain; + width: 100%; +`; + +const StyledLinkOverlay = styled.div<{ $backgroundColor: string }>` + align-items: center; + background-color: ${({ $backgroundColor }) => $backgroundColor}; + border-radius: ${themeCssVariables.border.radius.xs}; + bottom: -5px; + display: flex; + height: 14px; + justify-content: center; + position: absolute; + right: -6px; + width: 14px; +`; + +export type LinkIconWithLinkOverlayProps = { + link: string | null | undefined; + LinkIcon: IconComponent; + DefaultIcon: IconComponent; + color?: string | null; +}; + +export const LinkIconWithLinkOverlay = ({ + link, + LinkIcon, + DefaultIcon, + color: navItemColor, +}: LinkIconWithLinkOverlayProps) => { + const [localFailedLink, setLocalFailedLink] = useState(null); + const faviconUrl = getLinkFaviconUrl(link); + const linkKey = link ?? ''; + const isKnownFailed = failedFaviconUrls.has(linkKey); + const showFavicon = + isDefined(faviconUrl) && !isKnownFailed && localFailedLink !== linkKey; + + const linkStyle = getNavigationMenuItemIconStyleFromColor( + navItemColor ?? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK, + ); + + return ( + + + {showFavicon ? ( + { + if (isDefined(link)) failedFaviconUrls.add(link); + setLocalFailedLink(linkKey); + }} + /> + ) : ( + + )} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationItemDropTarget.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationItemDropTarget.tsx index 82a3672ca2..0f3e7e1c5b 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationItemDropTarget.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationItemDropTarget.tsx @@ -5,19 +5,13 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import { type NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants'; import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; -const StyledDropTarget = styled.div<{ - $isDragOver: boolean; - $isDropForbidden: boolean; - $compact?: boolean; -}>` +const StyledDropTarget = styled.div<{ $compact?: boolean }>` min-height: ${({ $compact }) => $compact ? 0 : themeCssVariables.spacing[2]}; position: relative; transition: all 150ms ease-in-out; - ${({ $isDragOver }) => - $isDragOver - ? ` + &[data-drag-over='true'] { background-color: ${themeCssVariables.background.transparent.blue}; &::before { @@ -28,29 +22,14 @@ const StyledDropTarget = styled.div<{ width: 100%; height: 2px; background-color: ${themeCssVariables.color.blue}; - border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0; + border-radius: ${themeCssVariables.border.radius.sm} + ${themeCssVariables.border.radius.sm} 0 0; } - ` - : ''} + } - ${({ $isDropForbidden }) => - $isDropForbidden - ? ` - background-color: ${themeCssVariables.background.transparent.danger}; + &[data-drop-forbidden='true'] { cursor: not-allowed; - - &::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 2px; - background-color: ${themeCssVariables.color.red}; - border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0; - } - ` - : ''} + } `; type NavigationItemDropTargetProps = { @@ -59,6 +38,7 @@ type NavigationItemDropTargetProps = { sectionId: NavigationSections; children?: ReactNode; compact?: boolean; + dropTargetIdOverride?: string; }; export const NavigationItemDropTarget = ({ @@ -67,19 +47,21 @@ export const NavigationItemDropTarget = ({ sectionId, children, compact = false, + dropTargetIdOverride, }: NavigationItemDropTargetProps) => { const { activeDropTargetId, forbiddenDropTargetId } = useContext( NavigationDropTargetContext, ); - const dropTargetId = `${sectionId}-${folderId ?? 'orphan'}-${index}`; + const dropTargetId = + dropTargetIdOverride ?? `${sectionId}-${folderId ?? 'orphan'}-${index}`; const isDragOver = activeDropTargetId === dropTargetId; const isDropForbidden = forbiddenDropTargetId === dropTargetId; return ( {children} diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuEditModeBar.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuEditModeBar.tsx index 13f9f06062..244630c715 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuEditModeBar.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuEditModeBar.tsx @@ -1,17 +1,18 @@ -import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; -import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState'; import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft'; import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; +import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; +import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; +import { AnimatePresence, motion } from 'framer-motion'; import { useContext, useState } from 'react'; import { SidePanelPages } from 'twenty-shared/types'; import { IconCheck, useIcons } from 'twenty-ui/display'; @@ -96,24 +97,34 @@ export const NavigationMenuEditModeBar = () => { const IconPaint = getIcon('IconPaint'); - if (!showNavigationMenuEditModeBar) { - return null; - } - return ( - - - - {t`Layout customization`} - - - + + {showNavigationMenuEditModeBar && ( + + + + + {t`Layout customization`} + + + + + )} + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemDroppable.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemDroppable.tsx index 59788856ec..2d17e48482 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemDroppable.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemDroppable.tsx @@ -1,5 +1,5 @@ -import { styled } from '@linaria/react'; import { Droppable } from '@hello-pangea/dnd'; +import { styled } from '@linaria/react'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection'; @@ -12,38 +12,26 @@ type NavigationMenuItemDroppableProps = { isWorkspaceSection?: boolean; }; -const StyledDroppableWrapper = styled.div<{ - isDraggingOver: boolean; - isDragIndicatorVisible: boolean; - showDropLine: boolean; -}>` +const StyledDroppableWrapper = styled.div` position: relative; transition: all 150ms ease-in-out; width: 100%; - ${({ isDraggingOver, isDragIndicatorVisible, showDropLine }) => - isDraggingOver && isDragIndicatorVisible - ? ` - background-color: ${themeCssVariables.background.transparent.blue}; + &[data-dragging-over='true'] { + background-color: ${themeCssVariables.background.transparent.blue}; + } - ${ - showDropLine - ? ` - &::before { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 2px; - background-color: ${themeCssVariables.color.blue}; - border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0; - } - ` - : '' - } - ` - : ''} + &[data-dragging-over='true'][data-show-drop-line='true']::before { + background-color: ${themeCssVariables.color.blue}; + border-radius: ${themeCssVariables.border.radius.sm} + ${themeCssVariables.border.radius.sm} 0 0; + bottom: 0; + content: ''; + height: 2px; + left: 0; + position: absolute; + width: 100%; + } `; export const NavigationMenuItemDroppable = ({ @@ -59,9 +47,12 @@ export const NavigationMenuItemDroppable = ({ {(provided, snapshot) => (
- deleteNavigationMenuItem(navigationMenuItem.id) - } + onClick={(e) => { + e.stopPropagation(); + deleteNavigationMenuItem(navigationMenuItem.id); + }} accent="tertiary" /> } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemFolders.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemFolders.tsx index a9df68bc4c..9c7585f831 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemFolders.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemFolders.tsx @@ -9,13 +9,7 @@ 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'; -type NavigationMenuItemFoldersProps = { - isNavigationSectionOpen: boolean; -}; - -export const NavigationMenuItemFolders = ({ - isNavigationSectionOpen, -}: NavigationMenuItemFoldersProps) => { +export const NavigationMenuItemFolders = () => { const [newFolderName, setNewFolderName] = useState(''); const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder(); @@ -61,10 +55,6 @@ export const NavigationMenuItemFolders = ({ setIsNavigationMenuItemFolderCreating(false); }; - if (!isNavigationSectionOpen) { - return null; - } - return ( <> {isNavigationMenuItemFolderCreating && ( diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx index 0db197a2ed..c4743fe4d9 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx @@ -1,9 +1,10 @@ import { isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; -import { Avatar, useIcons } from 'twenty-ui/display'; +import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay'; import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer'; import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; @@ -63,6 +64,17 @@ export const NavigationMenuItemIcon = ({ ); } + if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) { + return ( + + ); + } + const iconToUse = StandardIcon ?? (navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/ObjectIconWithViewOverlay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/ObjectIconWithViewOverlay.tsx index d7b8a27e75..131469f8c1 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/ObjectIconWithViewOverlay.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/ObjectIconWithViewOverlay.tsx @@ -1,6 +1,6 @@ import { styled } from '@linaria/react'; -import type { IconComponent } from 'twenty-ui/display'; import { useContext } from 'react'; +import type { IconComponent } from 'twenty-ui/display'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor'; @@ -37,13 +37,13 @@ const StyledViewOverlay = styled.div<{ $backgroundColor: string }>` align-items: center; background-color: ${({ $backgroundColor }) => $backgroundColor}; border-radius: 4px; - bottom: -7px; + bottom: -5px; display: flex; height: 14px; justify-content: center; position: absolute; - right: -7px; - width: 14px; + right: -6px; + width: 12px; `; export type ObjectIconWithViewOverlayProps = { @@ -74,8 +74,8 @@ export const ObjectIconWithViewOverlay = ({ diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitDroppableSlot.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitDroppableSlot.tsx new file mode 100644 index 0000000000..f390156292 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitDroppableSlot.tsx @@ -0,0 +1,51 @@ +import { useDroppable } from '@dnd-kit/react'; +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId'; +import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData'; + +const StyledSlotWrapper = styled.div<{ $empty: boolean }>` + min-height: 0; + ${({ $empty }) => + $empty ? `min-height: ${themeCssVariables.spacing[2]};` : ''} +`; + +const SLOT_COLLISION_PRIORITY = 1; + +export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 2; + +type WorkspaceDndKitDroppableSlotProps = { + droppableId: string; + index: number; + children?: ReactNode; + disabled?: boolean; + collisionPriority?: number; +}; + +export const WorkspaceDndKitDroppableSlot = ({ + droppableId, + index, + children, + disabled = false, + collisionPriority = SLOT_COLLISION_PRIORITY, +}: WorkspaceDndKitDroppableSlotProps) => { + const id = getDndKitDropTargetId(droppableId, index); + const data: DroppableData = { droppableId, index }; + const { ref } = useDroppable({ + id, + disabled, + collisionPriority, + data, + }); + + const isEmpty = + children == null || (Array.isArray(children) && children.length === 0); + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitSortableItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitSortableItem.tsx new file mode 100644 index 0000000000..fe9ea049a1 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceDndKitSortableItem.tsx @@ -0,0 +1,59 @@ +import { SortableKeyboardPlugin } from '@dnd-kit/dom/sortable'; +import { useSortable } from '@dnd-kit/react/sortable'; +import { styled } from '@linaria/react'; +import { type ReactNode } from 'react'; + +import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/SortableDropTargetRefContext'; + +const SORTABLE_COLLISION_PRIORITY = 3; + +const PLUGINS_WITHOUT_OPTIMISTIC = [SortableKeyboardPlugin]; + +const StyledSortableRoot = styled.div` + min-height: 0; + position: relative; +`; + +type WorkspaceDndKitSortableItemProps = { + children: ReactNode; + disabled?: boolean; + group: string; + id: string; + index: number; +}; + +export const WorkspaceDndKitSortableItem = ({ + id, + index, + group, + disabled = false, + children, +}: WorkspaceDndKitSortableItemProps) => { + const { handleRef, ref, targetRef } = useSortable({ + id, + index, + group, + collisionPriority: SORTABLE_COLLISION_PRIORITY, + data: { + sourceDroppableId: group, + sourceIndex: index, + }, + disabled, + transition: null, + plugins: PLUGINS_WITHOUT_OPTIMISTIC, + feedback: 'clone', + }); + + return ( + + { + ref(el); + handleRef?.(el); + }} + > + {children} + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem.tsx new file mode 100644 index 0000000000..b8a3775dec --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem.tsx @@ -0,0 +1,101 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon'; +import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel'; +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; +import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +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'; +import { coreViewsState } from '@/views/states/coreViewState'; +import { ViewKey } from '@/views/types/ViewKey'; +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; + +type WorkspaceNavigationMenuItemFolderSubItemProps = { + navigationMenuItem: ProcessedNavigationMenuItem; + index: number; + arrayLength: number; + selectedNavigationMenuItemIndex: number; + onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; + selectedNavigationMenuItemId: string | null; + isContextDragging: boolean; +}; + +export const WorkspaceNavigationMenuItemFolderSubItem = ({ + navigationMenuItem, + index, + arrayLength, + selectedNavigationMenuItemIndex, + onNavigationMenuItemClick, + selectedNavigationMenuItemId, + isContextDragging, +}: WorkspaceNavigationMenuItemFolderSubItemProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + const objectMetadataItems = useAtomStateValue(objectMetadataItemsState); + const coreViews = useAtomStateValue(coreViewsState); + const views = coreViews.map(convertCoreViewToView); + const objectMetadataItem = + navigationMenuItem.itemType === NavigationMenuItemType.VIEW || + navigationMenuItem.itemType === NavigationMenuItemType.RECORD + ? getObjectMetadataForNavigationMenuItem( + navigationMenuItem, + objectMetadataItems, + views, + ) + : null; + const isEditableInEditMode = + isNavigationMenuInEditMode && + isDefined(onNavigationMenuItemClick) && + (navigationMenuItem.itemType === NavigationMenuItemType.LINK || + isDefined(objectMetadataItem)); + + const handleEditModeClick = + isEditableInEditMode && isDefined(onNavigationMenuItemClick) + ? () => + onNavigationMenuItemClick({ + item: navigationMenuItem, + objectMetadataItem: objectMetadataItem ?? undefined, + }) + : undefined; + + return ( + ( + + )} + to={ + isContextDragging || handleEditModeClick + ? undefined + : navigationMenuItem.link + } + onClick={handleEditModeClick} + active={index === selectedNavigationMenuItemIndex} + isSelectedInEditMode={ + selectedNavigationMenuItemId === navigationMenuItem.id + } + subItemState={getNavigationSubItemLeftAdornment({ + index, + arrayLength, + selectedIndex: selectedNavigationMenuItemIndex, + })} + isDragging={isContextDragging} + triggerEvent="CLICK" + /> + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItems.tsx index 35a66dea6e..b60a1e75fe 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItems.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItems.tsx @@ -24,6 +24,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems'; +import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit'; import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -137,9 +138,6 @@ export const WorkspaceNavigationMenuItems = () => { }); }; - const isEditMode = - isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode; - if (loading) { return ; } @@ -151,7 +149,7 @@ export const WorkspaceNavigationMenuItems = () => { rightIcon={ isNavigationMenuItemEditingEnabled ? ( - {isEditMode ? ( + {isNavigationMenuInEditMode ? ( { onClick={handleAddMenuItem} /> ) : ( - +
+ +
)}
) : undefined } - onAddMenuItem={ - isNavigationMenuItemEditingEnabled && isEditMode - ? handleAddMenuItem - : undefined - } - isEditMode={isEditMode} selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode} onNavigationMenuItemClick={ - isEditMode ? handleNavigationMenuItemClick : undefined + isNavigationMenuInEditMode ? handleNavigationMenuItemClick : undefined } onActiveObjectMetadataItemClick={ isNavigationMenuItemEditingEnabled diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx index 27a609ebf6..27219b45cd 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx @@ -1,9 +1,7 @@ import { styled } from '@linaria/react'; -import { Droppable } from '@hello-pangea/dnd'; import { useLingui } from '@lingui/react/macro'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useContext } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import React, { useContext, useState } from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { IconChevronDown, @@ -15,36 +13,35 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection'; import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage'; +import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState'; +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; +import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; import { AnimatedExpandableContainer } from 'twenty-ui/layout'; import { useIsMobile } from 'twenty-ui/utilities'; -import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable'; -import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon'; -import { WorkspaceNavigationMenuItemFolderDragClone } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderDragClone'; +import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget'; +import { + FOLDER_HEADER_SLOT_COLLISION_PRIORITY, + WorkspaceDndKitDroppableSlot, +} from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot'; +import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem'; +import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem'; import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault'; import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; -import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants'; +import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext'; +import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/SortableDropTargetRefContext'; import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; -import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState'; -import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel'; -import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; -import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId'; import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; -import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState'; import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment'; -import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -import { coreViewsState } from '@/views/states/coreViewState'; -import { ViewKey } from '@/views/types/ViewKey'; -import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>` border: ${({ $isSelectedInEditMode }) => @@ -52,15 +49,20 @@ const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>` ? `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<{ - $compact: boolean; -}>` +const StyledFolderDroppableContent = styled.div` display: flex; flex-direction: column; - padding-bottom: ${({ $compact }) => - $compact ? 0 : themeCssVariables.spacing[2]}; `; const StyledFolderExpandableWrapper = styled.div` @@ -76,7 +78,6 @@ type WorkspaceNavigationMenuItemsFolderProps = { folderColor?: string | null; navigationMenuItems: ProcessedNavigationMenuItem[]; isGroup: boolean; - isEditMode?: boolean; isSelectedInEditMode?: boolean; onEditModeClick?: () => void; onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; @@ -91,40 +92,31 @@ export const WorkspaceNavigationMenuItemsFolder = ({ folderColor, navigationMenuItems, isGroup, - isEditMode = false, isSelectedInEditMode = false, onEditModeClick, onNavigationMenuItemClick, selectedNavigationMenuItemId = null, isDragging = false, }: WorkspaceNavigationMenuItemsFolderProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); const { theme } = useContext(ThemeContext); const { getIcon } = useIcons(); const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); - const objectMetadataItems = useAtomStateValue(objectMetadataItemsState); - const coreViews = useAtomStateValue(coreViewsState); - const views = coreViews.map(convertCoreViewToView); - const location = useLocation(); - const navigate = useNavigate(); - const currentPath = location.pathname; - const currentViewPath = location.pathname + location.search; const isMobile = useIsMobile(); - const { t } = useLingui(); - const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] = - useAtomState(openNavigationMenuItemFolderIdsState); - - const setCurrentNavigationMenuItemFolderId = useSetAtomState( - currentNavigationMenuItemFolderIdState, - ); + const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = + useWorkspaceFolderOpenState({ folderId, navigationMenuItems }); const { openAddItemToFolderPage } = useOpenAddItemToFolderPage(); + const sidePanelPage = useAtomStateValue(sidePanelPageState); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); - const isOpen = openNavigationMenuItemFolderIds.includes(folderId); - - const folderContentLengthForTree = - isEditMode && isSelectedInEditMode - ? navigationMenuItems.length + 1 - : navigationMenuItems.length; + const folderContentLengthForTree = isNavigationMenuInEditMode + ? navigationMenuItems.length + 1 + : navigationMenuItems.length; const handleAddMenuItemToFolder = () => { openAddItemToFolderPage({ @@ -134,208 +126,180 @@ export const WorkspaceNavigationMenuItemsFolder = ({ }); }; - const handleToggle = () => { - if (isMobile) { - setCurrentNavigationMenuItemFolderId((prev) => - prev === folderId ? null : folderId, - ); - } else { - setOpenNavigationMenuItemFolderIds((current) => - isOpen - ? current.filter((id) => id !== folderId) - : [...current, folderId], - ); - } - - if (!isOpen) { - const firstNonLinkItem = navigationMenuItems.find( - (item) => - item.itemType !== NavigationMenuItemType.LINK && - isNonEmptyString(item.link), - ); - if (isDefined(firstNonLinkItem?.link)) { - navigate(firstNonLinkItem.link); + const shouldUseEditModeClick = + isNavigationMenuInEditMode && isDefined(onEditModeClick); + const handleClick = shouldUseEditModeClick + ? (e?: React.MouseEvent) => { + e?.stopPropagation(); + if (isSelectedInEditMode) { + handleToggle(); + } else { + onEditModeClick?.(); + } } - } - }; + : handleToggle; - const shouldUseEditModeClick = isEditMode && isDefined(onEditModeClick); - const handleClick = shouldUseEditModeClick ? onEditModeClick : handleToggle; + const [skipInitialExpandAnimation] = useState(() => isOpen); - const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex( - (item) => - isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item), - ); - - const navigationMenuItemFolderContentLength = navigationMenuItems.length; const { isDragging: isContextDragging } = useContext( NavigationMenuItemDragContext, ); + const setSortableDropTargetRef = useContext(SortableDropTargetRefContext); const folderContentDropDisabled = useIsDropDisabledForSection(true); - return ( - - - - - ) : ( - - ) - } - /> - + 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 = + isNavigationMenuInEditMode || navigationMenuItems.length === 0; + const headerItem = ( + = 0} + onClick={handleClick} + className="navigation-drawer-item" + triggerEvent="CLICK" + preventCollapseOnMobile={isMobile} + isDragging={isDragging} + alwaysShowRightOptions + rightOptions={ +
{ + e.stopPropagation(); + handleToggle(); + }} + > + {isOpen ? ( + + ) : ( + + )} +
+ } + /> + ); + + return ( + + +
+ + {headerItem} + +
- ( - + {navigationMenuItems.map((navigationMenuItem, index) => ( + + + + + + + ))} + + - )} - getContainerForClone={() => document.body} - > - {(provided) => ( - - {navigationMenuItems.map((navigationMenuItem, index) => { - const objectMetadataItem = - navigationMenuItem.itemType === - NavigationMenuItemType.VIEW || - navigationMenuItem.itemType === - NavigationMenuItemType.RECORD - ? getObjectMetadataForNavigationMenuItem( - navigationMenuItem, - objectMetadataItems, - views, - ) - : null; - const handleEditModeClick = - isEditMode && - isDefined(onNavigationMenuItemClick) && - (navigationMenuItem.itemType === - NavigationMenuItemType.LINK || - isDefined(objectMetadataItem)) - ? () => - onNavigationMenuItemClick({ - item: navigationMenuItem, - objectMetadataItem: - objectMetadataItem ?? undefined, - }) - : undefined; - - return ( - ( - - )} - to={ - isContextDragging || handleEditModeClick - ? undefined - : navigationMenuItem.link - } - onClick={handleEditModeClick} - active={index === selectedNavigationMenuItemIndex} - isSelectedInEditMode={ - selectedNavigationMenuItemId === - navigationMenuItem.id - } - subItemState={getNavigationSubItemLeftAdornment({ - index, - arrayLength: folderContentLengthForTree, - selectedIndex: selectedNavigationMenuItemIndex, - })} - isDragging={isContextDragging} - triggerEvent="CLICK" - /> - } - /> - ); - })} - {provided.placeholder} - - )} - - {isEditMode && isSelectedInEditMode && ( - - )} + {isNavigationMenuInEditMode && ( + + )} + +
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/constants/DndKitDropTargetIdSeparator.ts b/packages/twenty-front/src/modules/navigation-menu-item/constants/DndKitDropTargetIdSeparator.ts new file mode 100644 index 0000000000..570d00569d --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/constants/DndKitDropTargetIdSeparator.ts @@ -0,0 +1 @@ +export const DND_KIT_DROP_TARGET_ID_SEPARATOR = '::'; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/contexts/SortableDropTargetRefContext.tsx b/packages/twenty-front/src/modules/navigation-menu-item/contexts/SortableDropTargetRefContext.tsx new file mode 100644 index 0000000000..7bfe66678f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/contexts/SortableDropTargetRefContext.tsx @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +export const SortableDropTargetRefContext = createContext< + ((element: Element | null) => void) | null +>(null); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleAddToNavigationDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleAddToNavigationDrop.ts index 911e4f5b1d..0f18e2f96a 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleAddToNavigationDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleAddToNavigationDrop.ts @@ -17,7 +17,6 @@ import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addT import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState'; -import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/utils/getObjectMetadataIdsInDraft'; import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId'; @@ -45,9 +44,6 @@ export const useHandleAddToNavigationDrop = () => { const { objectMetadataItems } = useObjectMetadataItems(); const coreViews = useAtomStateValue(coreViewsState); const { getIcon } = useIcons(); - const setSelectedNavigationMenuItemInEditMode = useSetAtomState( - selectedNavigationMenuItemInEditModeState, - ); const setIsNavigationMenuInEditMode = useSetAtomState( isNavigationMenuInEditModeState, ); @@ -92,11 +88,13 @@ export const useHandleAddToNavigationDrop = () => { const openEditForNewNavItem = ( newItemId: string, - options: Parameters[0], + options: Omit< + Parameters[0], + 'itemId' + >, ) => { setIsNavigationMenuInEditMode(true); - setSelectedNavigationMenuItemInEditMode(newItemId); - openNavigationMenuItemInSidePanel(options); + openNavigationMenuItemInSidePanel({ ...options, itemId: newItemId }); }; switch (payload.type) { @@ -223,7 +221,6 @@ export const useHandleAddToNavigationDrop = () => { openNavigationMenuItemInSidePanel, setOpenNavigationMenuItemFolderIds, setIsNavigationMenuInEditMode, - setSelectedNavigationMenuItemInEditMode, workspaceNavigationMenuItems, store, ], diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts index e8296ba7dc..edd52f6212 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop.ts @@ -5,12 +5,14 @@ import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; import { matchesWorkspaceFolderId, validateAndExtractWorkspaceFolderId, } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { isDefined } from 'twenty-shared/utils'; import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData'; @@ -83,6 +85,13 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => { const destinationFolderId = validateAndExtractWorkspaceFolderId( destination.droppableId, ); + + if ( + isNavigationMenuItemFolder(draggedItem) && + isDefined(destinationFolderId) + ) { + return; + } const sourceFolderId = validateAndExtractWorkspaceFolderId( source.droppableId, ); @@ -108,69 +117,41 @@ export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => { } const isSameList = sourceFolderId === destinationFolderId; - let reorderedDestinationList: NavigationMenuItem[]; if (isSameList) { const listWithoutDragged = sourceList.filter( (item) => item.id !== draggableId, ); - reorderedDestinationList = [ - ...listWithoutDragged.slice(0, destination.index), - draggedItem, - ...listWithoutDragged.slice(destination.index), - ]; - } else { - const destinationListWithInsertedItem = [ - ...destinationList.slice(0, destination.index), - { ...draggedItem, folderId: destinationFolderId }, - ...destinationList.slice(destination.index), - ]; - reorderedDestinationList = destinationListWithInsertedItem; - } - - const destinationWithNormalizedPositions = reorderedDestinationList.map( - (item, index) => ({ - ...item, - position: index, - folderId: isSameList ? item.folderId : destinationFolderId, - }), - ); - - const positionUpdates = new Map< - string, - { position: number; folderId: string | null } - >(); - destinationWithNormalizedPositions.forEach((item) => { - positionUpdates.set(item.id, { - position: item.position, - folderId: item.folderId ?? null, - }); - }); - - if (!isSameList) { - const sourceListWithoutDragged = sourceList.filter( - (item) => item.id !== draggableId, + const prevItem = listWithoutDragged[destination.index - 1]; + const nextItem = listWithoutDragged[destination.index]; + const newPosition = getPositionBetween( + prevItem?.position, + nextItem?.position, ); - sourceListWithoutDragged.forEach((item, index) => { - positionUpdates.set(item.id, { - position: index, - folderId: sourceFolderId, - }); - }); + const updatedDraft = navigationMenuItemsDraft.map( + (item): NavigationMenuItem => + item.id === draggableId ? { ...item, position: newPosition } : item, + ); + setNavigationMenuItemsDraft(updatedDraft); + return; } + const prevItem = destinationList[destination.index - 1]; + const nextItem = destinationList[destination.index]; + const newPosition = getPositionBetween( + prevItem?.position, + nextItem?.position, + ); const updatedDraft = navigationMenuItemsDraft.map( (item): NavigationMenuItem => { - const update = positionUpdates.get(item.id); - if (!update) return item; + if (item.id !== draggableId) return item; return { ...item, - position: update.position, - folderId: update.folderId, + position: newPosition, + folderId: destinationFolderId, }; }, ); - setNavigationMenuItemsDraft(updatedDraft); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove.ts index e07a9e975d..4ea3d470ae 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove.ts @@ -1,66 +1,78 @@ import { isDefined } from 'twenty-shared/utils'; -import type { NavigationMenuItem } from '~/generated-metadata/graphql'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -const swapPositionsInDraft = ( - draft: NavigationMenuItem[], - itemA: NavigationMenuItem, - itemB: NavigationMenuItem, -): NavigationMenuItem[] => - draft.map((item) => { - if (item.id === itemA.id) { - return { ...item, position: itemB.position }; - } - if (item.id === itemB.id) { - return { ...item, position: itemA.position }; - } - return item; - }); +import { useWorkspaceSectionItems } from './useWorkspaceSectionItems'; export const useNavigationMenuItemMoveRemove = () => { const setNavigationMenuItemsDraft = useSetAtomState( navigationMenuItemsDraftState, ); + const items = useWorkspaceSectionItems(); + const visibleItemIds = new Set(items.map((item) => item.id)); const moveUp = (navigationMenuItemId: string) => { setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; + if (!draft) { + return draft; + } const currentItem = draft.find( (item) => item.id === navigationMenuItemId, ); - if (!currentItem) return draft; + if (!currentItem) { + return draft; + } const folderId = currentItem.folderId ?? null; const siblings = draft - .filter((item) => (item.folderId ?? null) === folderId) + .filter( + (item) => + (item.folderId ?? null) === folderId && visibleItemIds.has(item.id), + ) .sort((a, b) => a.position - b.position); const currentIndex = siblings.findIndex( (item) => item.id === navigationMenuItemId, ); - if (currentIndex <= 0) return draft; + if (currentIndex <= 0) { + return draft; + } - const itemAbove = siblings[currentIndex - 1]; - return swapPositionsInDraft(draft, currentItem, itemAbove); + const prev = siblings[currentIndex - 1]; + const prevPrev = siblings[currentIndex - 2]; + const newPosition = getPositionBetween(prevPrev?.position, prev.position); + + return draft.map((item) => + item.id === navigationMenuItemId + ? { ...item, position: newPosition } + : item, + ); }); }; const moveDown = (navigationMenuItemId: string) => { setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; + if (!draft) { + return draft; + } const currentItem = draft.find( (item) => item.id === navigationMenuItemId, ); - if (!currentItem) return draft; + if (!currentItem) { + return draft; + } const folderId = currentItem.folderId ?? null; const siblings = draft - .filter((item) => (item.folderId ?? null) === folderId) + .filter( + (item) => + (item.folderId ?? null) === folderId && visibleItemIds.has(item.id), + ) .sort((a, b) => a.position - b.position); const currentIndex = siblings.findIndex( @@ -70,19 +82,30 @@ export const useNavigationMenuItemMoveRemove = () => { return draft; } - const itemBelow = siblings[currentIndex + 1]; - return swapPositionsInDraft(draft, currentItem, itemBelow); + const next = siblings[currentIndex + 1]; + const nextNext = siblings[currentIndex + 2]; + const newPosition = getPositionBetween(next.position, nextNext?.position); + + return draft.map((item) => + item.id === navigationMenuItemId + ? { ...item, position: newPosition } + : item, + ); }); }; const remove = (navigationMenuItemId: string) => { setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; + if (!draft) { + return draft; + } const itemToRemove = draft.find( (item) => item.id === navigationMenuItemId, ); - if (!itemToRemove) return draft; + if (!itemToRemove) { + return draft; + } const isFolder = isNavigationMenuItemFolder(itemToRemove); @@ -103,10 +126,14 @@ export const useNavigationMenuItemMoveRemove = () => { targetFolderId: string | null, ) => { setNavigationMenuItemsDraft((draft) => { - if (!draft) return draft; + if (!draft) { + return draft; + } const itemToMove = draft.find((item) => item.id === navigationMenuItemId); - if (!itemToMove) return draft; + if (!itemToMove) { + return draft; + } const isFolder = isNavigationMenuItemFolder(itemToMove); if (isFolder && targetFolderId === navigationMenuItemId) { diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenAddItemToFolderPage.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenAddItemToFolderPage.ts index cb12604cfd..6e7aeec195 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenAddItemToFolderPage.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenAddItemToFolderPage.ts @@ -1,5 +1,6 @@ -import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; +import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; +import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useLingui } from '@lingui/react/macro'; import { SidePanelPages } from 'twenty-shared/types'; @@ -17,12 +18,16 @@ export const useOpenAddItemToFolderPage = () => { const setAddMenuItemInsertionContext = useSetAtomState( addMenuItemInsertionContextState, ); + const setSelectedNavigationMenuItemInEditMode = useSetAtomState( + selectedNavigationMenuItemInEditModeState, + ); const openAddItemToFolderPage = ({ targetFolderId, targetIndex, resetNavigationStack = true, }: OpenAddItemToFolderPageParams) => { + setSelectedNavigationMenuItemInEditMode(null); setAddMenuItemInsertionContext({ targetFolderId, targetIndex, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel.ts index d1bdc920e5..7c98eaea43 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel.ts @@ -1,19 +1,30 @@ +import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { SidePanelPages } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import type { IconComponent } from 'twenty-ui/display'; export const useOpenNavigationMenuItemInSidePanel = () => { const { navigateSidePanel } = useNavigateSidePanel(); + const setSelectedNavigationMenuItemInEditMode = useSetAtomState( + selectedNavigationMenuItemInEditModeState, + ); const openNavigationMenuItemInSidePanel = ({ + itemId, pageTitle, pageIcon, focusTitleInput = false, }: { + itemId?: string; pageTitle: string; pageIcon: IconComponent; focusTitleInput?: boolean; }) => { + if (isDefined(itemId)) { + setSelectedNavigationMenuItemInEditMode(itemId); + } navigateSidePanel({ page: SidePanelPages.NavigationMenuItemEdit, pageTitle, diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft.ts index 5e14c3ef1f..0a4d1b2699 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useSaveNavigationMenuItemsDraft.ts @@ -1,17 +1,16 @@ import { useCallback } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { - type CreateNavigationMenuItemInput, - useCreateNavigationMenuItemMutation, -} from '~/generated-metadata/graphql'; +import { useCreateNavigationMenuItemMutation } from '~/generated-metadata/graphql'; import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem'; import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; +import { buildCreateNavigationMenuItemInput } from '@/navigation-menu-item/utils/buildCreateNavigationMenuItemInput'; import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink'; +import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation'; import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState'; import { useStore } from 'jotai'; @@ -76,37 +75,39 @@ export const useSaveNavigationMenuItemsDraft = () => { await deleteNavigationMenuItem(draftItem.id); } - const idsToCreateIncludingRecreated = [...idsToCreate, ...idsToRecreate]; + const itemsToCreate = [...idsToCreate, ...idsToRecreate]; + const foldersToCreate = itemsToCreate.filter(isNavigationMenuItemFolder); + const nonFoldersToCreate = itemsToCreate.filter( + (item) => !isNavigationMenuItemFolder(item), + ); - for (const draftItem of idsToCreateIncludingRecreated) { - const input: CreateNavigationMenuItemInput = { - position: Math.max(0, Math.round(draftItem.position)), - }; + const createdFolderIdByDraftId = new Map(); + const resolveFolderId = (draftFolderId: string): string => + createdFolderIdByDraftId.get(draftFolderId) ?? draftFolderId; - if (isNavigationMenuItemFolder(draftItem)) { - input.name = draftItem.name ?? undefined; - input.icon = draftItem.icon ?? null; - } else if (isNavigationMenuItemLink(draftItem)) { - input.name = draftItem.name ?? 'Link'; - const linkUrl = (draftItem.link ?? '').trim(); - input.link = - linkUrl.startsWith('http://') || linkUrl.startsWith('https://') - ? linkUrl - : linkUrl - ? `https://${linkUrl}` - : undefined; - } else if (isDefined(draftItem.viewId)) { - input.viewId = draftItem.viewId; - } else if (isDefined(draftItem.targetRecordId)) { - input.targetRecordId = draftItem.targetRecordId; - input.targetObjectMetadataId = - draftItem.targetObjectMetadataId ?? undefined; - } - - if (isDefined(draftItem.folderId)) { - input.folderId = draftItem.folderId; + const orderedFolders = orderFoldersForCreation( + foldersToCreate, + prefetchIds, + ); + for (const draftItem of orderedFolders) { + const input = buildCreateNavigationMenuItemInput( + draftItem, + resolveFolderId, + ); + const result = await createNavigationMenuItemMutation({ + variables: { input }, + }); + const created = result.data?.createNavigationMenuItem; + if (isDefined(created?.id)) { + createdFolderIdByDraftId.set(draftItem.id, created.id); } + } + for (const draftItem of nonFoldersToCreate) { + const input = buildCreateNavigationMenuItemInput( + draftItem, + resolveFolderId, + ); await createNavigationMenuItemMutation({ variables: { input }, }); @@ -150,10 +151,13 @@ export const useSaveNavigationMenuItemsDraft = () => { } = { id: draftItem.id }; if (positionChanged) { - updateInput.position = Math.max(0, Math.round(draftItem.position)); + updateInput.position = draftItem.position; } if (folderIdChanged) { - updateInput.folderId = draftItem.folderId ?? null; + updateInput.folderId = + draftItem.folderId != null + ? resolveFolderId(draftItem.folderId) + : null; } if (nameChanged && isNavigationMenuItemFolder(draftItem)) { updateInput.name = draftItem.name ?? undefined; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceFolderOpenState.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceFolderOpenState.ts new file mode 100644 index 0000000000..fa1078afb2 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceFolderOpenState.ts @@ -0,0 +1,72 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import { isDefined } from 'twenty-shared/utils'; +import { useIsMobile } from 'twenty-ui/utilities'; +import { isNonEmptyString } from '@sniptt/guards'; + +import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState'; +import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState'; +import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; + +type UseWorkspaceFolderOpenStateParams = { + folderId: string; + navigationMenuItems: ProcessedNavigationMenuItem[]; +}; + +export const useWorkspaceFolderOpenState = ({ + folderId, + navigationMenuItems, +}: UseWorkspaceFolderOpenStateParams) => { + const location = useLocation(); + const navigate = useNavigate(); + const currentPath = location.pathname; + const currentViewPath = location.pathname + location.search; + const isMobile = useIsMobile(); + + const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] = + useAtomState(openNavigationMenuItemFolderIdsState); + const setCurrentNavigationMenuItemFolderId = useSetAtomState( + currentNavigationMenuItemFolderIdState, + ); + + const isOpen = openNavigationMenuItemFolderIds.includes(folderId); + + const handleToggle = () => { + if (isMobile) { + setCurrentNavigationMenuItemFolderId((prev) => + prev === folderId ? null : folderId, + ); + } else { + setOpenNavigationMenuItemFolderIds((current) => + isOpen + ? current.filter((id) => id !== folderId) + : [...current, folderId], + ); + } + + if (!isOpen) { + const firstNonLinkItem = navigationMenuItems.find( + (item) => + item.itemType !== NavigationMenuItemType.LINK && + isNonEmptyString(item.link), + ); + if (isDefined(firstNonLinkItem?.link)) { + navigate(firstNonLinkItem.link); + } + } + }; + + const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex( + (item) => + isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item), + ); + + return { + isOpen, + handleToggle, + selectedNavigationMenuItemIndex, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceSectionItems.ts b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceSectionItems.ts index dcc0ad81b6..325f09c58f 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceSectionItems.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/hooks/useWorkspaceSectionItems.ts @@ -5,8 +5,10 @@ import { NavigationMenuItemType } from '@/navigation-menu-item/constants/Navigat import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item'; import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { coreViewsState } from '@/views/states/coreViewState'; import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; @@ -36,6 +38,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => { useNavigationMenuItemsByFolder(); const coreViews = useAtomStateValue(coreViewsState); const objectMetadataItems = useAtomStateValue(objectMetadataItemsState); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const views = coreViews.map(convertCoreViewToView); @@ -76,7 +79,13 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => { objectMetadataItems, views, ); - if (isDefined(objectMetadataItem)) { + if ( + isDefined(objectMetadataItem) && + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + objectMetadataItem.id, + ).canReadObjectRecords + ) { acc.push(processedItem); } } diff --git a/packages/twenty-front/src/modules/navigation-menu-item/types/AddMenuItemInsertionContext.ts b/packages/twenty-front/src/modules/navigation-menu-item/types/AddMenuItemInsertionContext.ts index 54c1a526fa..54d3571c9d 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/types/AddMenuItemInsertionContext.ts +++ b/packages/twenty-front/src/modules/navigation-menu-item/types/AddMenuItemInsertionContext.ts @@ -1,4 +1,5 @@ export type AddMenuItemInsertionContext = { targetFolderId: string | null; targetIndex: number; + disableDrag?: boolean; }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/extractDomainFromUrl.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/extractDomainFromUrl.test.ts new file mode 100644 index 0000000000..63bfbc1633 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/extractDomainFromUrl.test.ts @@ -0,0 +1,15 @@ +import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl'; + +describe('extractDomainFromUrl', () => { + it('returns hostname and strips www prefix', () => { + expect(extractDomainFromUrl('https://www.example.com/path')).toBe( + 'example.com', + ); + expect(extractDomainFromUrl('https://example.com')).toBe('example.com'); + }); + + it('returns undefined for invalid URLs', () => { + expect(extractDomainFromUrl('not-a-url')).toBeUndefined(); + expect(extractDomainFromUrl('')).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/getPositionBetween.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/getPositionBetween.test.ts new file mode 100644 index 0000000000..ef1fee2df0 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/getPositionBetween.test.ts @@ -0,0 +1,27 @@ +import { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween'; + +describe('getPositionBetween', () => { + it('returns next - 1 when only next is defined (insert before)', () => { + expect(getPositionBetween(null, 10)).toBe(9); + expect(getPositionBetween(undefined, 1)).toBe(0); + }); + + it('returns prev + 1 when only prev is defined (insert after)', () => { + expect(getPositionBetween(5, null)).toBe(6); + expect(getPositionBetween(0, undefined)).toBe(1); + }); + + it('returns midpoint when both are defined and different', () => { + expect(getPositionBetween(0, 10)).toBe(5); + expect(getPositionBetween(1, 3)).toBe(2); + }); + + it('returns prev - 1 when both defined and equal', () => { + expect(getPositionBetween(5, 5)).toBe(4); + }); + + it('returns 0 when both are null or undefined', () => { + expect(getPositionBetween(null, null)).toBe(0); + expect(getPositionBetween(undefined, undefined)).toBe(0); + }); +}); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/orderFoldersForCreation.test.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/orderFoldersForCreation.test.ts new file mode 100644 index 0000000000..44ed5e6dda --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/__tests__/orderFoldersForCreation.test.ts @@ -0,0 +1,35 @@ +import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation'; + +describe('orderFoldersForCreation', () => { + it('returns empty array for empty input', () => { + expect(orderFoldersForCreation([], new Set())).toEqual([]); + }); + + it('orders so parent folders come before their children', () => { + const parent = { id: 'parent', folderId: null }; + const child = { id: 'child', folderId: 'parent' }; + expect(orderFoldersForCreation([child, parent], new Set())).toEqual([ + parent, + child, + ]); + }); + + it('treats folderId in existingIds as already created so child can be placed', () => { + const child = { id: 'child', folderId: 'existing-parent' }; + expect( + orderFoldersForCreation([child], new Set(['existing-parent'])), + ).toEqual([child]); + }); + + it('orders multiple levels: root first, then children in dependency order', () => { + const a = { id: 'a', folderId: undefined }; + const b = { id: 'b', folderId: 'a' }; + const c = { id: 'c', folderId: 'b' }; + expect(orderFoldersForCreation([c, a, b], new Set())).toEqual([a, b, c]); + }); + + it('leaves out folders whose parent is missing and not in existingIds', () => { + const child = { id: 'child', folderId: 'missing-parent' }; + expect(orderFoldersForCreation([child], new Set())).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/buildCreateNavigationMenuItemInput.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/buildCreateNavigationMenuItemInput.ts new file mode 100644 index 0000000000..d4625badc6 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/buildCreateNavigationMenuItemInput.ts @@ -0,0 +1,48 @@ +import type { + CreateNavigationMenuItemInput, + NavigationMenuItem, +} from '~/generated-metadata/graphql'; + +import { isDefined } from 'twenty-shared/utils'; + +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; +import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink'; + +export const buildCreateNavigationMenuItemInput = ( + draftItem: NavigationMenuItem, + resolveFolderId: (draftFolderId: string) => string, +): CreateNavigationMenuItemInput => { + const input: CreateNavigationMenuItemInput = { + position: draftItem.position, + }; + + if (isNavigationMenuItemFolder(draftItem)) { + input.name = draftItem.name ?? undefined; + input.icon = draftItem.icon ?? null; + } else if (isNavigationMenuItemLink(draftItem)) { + input.name = draftItem.name ?? 'Link'; + const linkUrl = (draftItem.link ?? '').trim(); + input.link = + linkUrl.startsWith('http://') || linkUrl.startsWith('https://') + ? linkUrl + : linkUrl + ? `https://${linkUrl}` + : undefined; + } else if (isDefined(draftItem.viewId)) { + input.viewId = draftItem.viewId; + } else if (isDefined(draftItem.targetRecordId)) { + input.targetRecordId = draftItem.targetRecordId; + input.targetObjectMetadataId = + draftItem.targetObjectMetadataId ?? undefined; + } + + if (isDefined(draftItem.folderId)) { + input.folderId = resolveFolderId(draftItem.folderId); + } + + if (isDefined(draftItem.color)) { + input.color = draftItem.color; + } + + return input; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/extractDomainFromUrl.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/extractDomainFromUrl.ts new file mode 100644 index 0000000000..1f07dff8d5 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/extractDomainFromUrl.ts @@ -0,0 +1,8 @@ +export const extractDomainFromUrl = (url: string): string | undefined => { + try { + const hostname = new URL(url).hostname; + return hostname.replace(/^www\./, '') || undefined; + } catch { + return undefined; + } +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/getDndKitDropTargetId.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/getDndKitDropTargetId.ts new file mode 100644 index 0000000000..cdfe08c7de --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/getDndKitDropTargetId.ts @@ -0,0 +1,6 @@ +import { DND_KIT_DROP_TARGET_ID_SEPARATOR } from '@/navigation-menu-item/constants/DndKitDropTargetIdSeparator'; + +export const getDndKitDropTargetId = ( + droppableId: string, + index: number, +): string => `${droppableId}${DND_KIT_DROP_TARGET_ID_SEPARATOR}${index}`; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination.ts new file mode 100644 index 0000000000..284f1485e3 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination.ts @@ -0,0 +1,22 @@ +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { NavigationSections } from '@/navigation-menu-item/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; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts new file mode 100644 index 0000000000..10f39310d3 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts @@ -0,0 +1,20 @@ +import { getLogoUrlFromDomainName } from 'twenty-shared/utils'; + +export const getLinkFaviconUrl = ( + link: string | null | undefined, +): string | undefined => { + const trimmed = (link ?? '').trim(); + if (!trimmed) { + return undefined; + } + const normalized = + trimmed.startsWith('http://') || trimmed.startsWith('https://') + ? trimmed + : `https://${trimmed}`; + try { + const hostname = new URL(normalized).hostname; + return getLogoUrlFromDomainName(hostname); + } catch { + return undefined; + } +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/getPositionBetween.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/getPositionBetween.ts new file mode 100644 index 0000000000..55b4287c2c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/getPositionBetween.ts @@ -0,0 +1,18 @@ +import { isDefined } from 'twenty-shared/utils'; + +export const getPositionBetween = ( + prevPosition: number | null | undefined, + nextPosition: number | null | undefined, +): number => { + if (!isDefined(prevPosition) && isDefined(nextPosition)) + return nextPosition - 1; + if (isDefined(prevPosition) && !isDefined(nextPosition)) + return prevPosition + 1; + if (isDefined(prevPosition) && isDefined(nextPosition)) { + if (prevPosition === nextPosition) { + return prevPosition - 1; + } + return (prevPosition + nextPosition) / 2; + } + return 0; +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/orderFoldersForCreation.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/orderFoldersForCreation.ts new file mode 100644 index 0000000000..88ef500d66 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/orderFoldersForCreation.ts @@ -0,0 +1,24 @@ +export const orderFoldersForCreation = < + T extends { id: string; folderId?: string | null }, +>( + folders: T[], + existingIds: Set, +): T[] => { + const result: T[] = []; + let remaining = [...folders]; + + while (remaining.length > 0) { + const readyIndex = remaining.findIndex( + (folder) => + !folder.folderId || + existingIds.has(folder.folderId) || + result.some((r) => r.id === folder.folderId), + ); + if (readyIndex === -1) break; + + const [ready] = remaining.splice(readyIndex, 1); + result.push(ready); + } + + return result; +}; diff --git a/packages/twenty-front/src/modules/navigation/components/FavoritesDragDropProviderContent.tsx b/packages/twenty-front/src/modules/navigation/components/FavoritesDragDropProviderContent.tsx new file mode 100644 index 0000000000..f1ebd14d00 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/FavoritesDragDropProviderContent.tsx @@ -0,0 +1,177 @@ +import { + DragDropContext, + type DragStart, + type DropResult, + type OnDragUpdateResponder, + type ResponderProvided, +} from '@hello-pangea/dnd'; +import { useState, type ReactNode } from 'react'; + +import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext'; +import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop'; +import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext'; +import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; +import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext'; +import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop'; +import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop'; +import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState'; +import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState'; +import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination'; +import { getFavoritesDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getFavoritesDropTargetIdFromDestination'; +import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId'; +import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId'; +import { useStore } from 'jotai'; +import { isDefined } from 'twenty-shared/utils'; + +type FavoritesDragDropProviderContentProps = { + children: ReactNode; +}; + +export const FavoritesDragDropProviderContent = ({ + children, +}: FavoritesDragDropProviderContentProps) => { + const [isDragging, setIsDragging] = useState(false); + const [sourceDroppableId, setSourceDroppableId] = useState( + null, + ); + const [activeDropTargetId, setActiveDropTargetId] = useState( + null, + ); + const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< + string | null + >(null); + const [ + addToNavigationFallbackDestination, + setAddToNavigationFallbackDestination, + ] = useState<{ droppableId: string; index: number } | null>(null); + + const store = useStore(); + const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); + const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop(); + const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop(); + const { handleNavigationMenuItemDragAndDrop } = + useHandleNavigationMenuItemDragAndDrop(); + + const isFavoritesDroppableId = (droppableId: string) => + droppableId === + NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS || + droppableId.startsWith('folder-'); + + const orphanItemCount = workspaceNavigationMenuItems.filter( + (item: { folderId?: string | null }) => !isDefined(item.folderId), + ).length; + + const handleDragStart = (dragStart: DragStart) => { + setIsDragging(true); + setSourceDroppableId(dragStart.source.droppableId); + if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { + const defaultDestination = { + droppableId: + NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, + index: orphanItemCount, + }; + setAddToNavigationFallbackDestination(defaultDestination); + setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination)); + } + }; + + const handleDragUpdate = (update: Parameters[0]) => { + const { source, destination } = update; + + if (source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { + if ( + destination !== null && + isWorkspaceDroppableId(destination.droppableId) + ) { + setAddToNavigationFallbackDestination(destination); + const dropTargetId = getDropTargetIdFromDestination(destination); + setActiveDropTargetId(dropTargetId); + + const payload = + store + .get(addToNavPayloadRegistryState.atom) + .get(update.draggableId) ?? null; + const folderId = validateAndExtractWorkspaceFolderId( + destination.droppableId, + ); + const isFolderOverFolder = + payload?.type === 'folder' && folderId !== null; + setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null); + } else { + setForbiddenDropTargetId(null); + const fallback = addToNavigationFallbackDestination; + setActiveDropTargetId( + fallback ? getDropTargetIdFromDestination(fallback) : null, + ); + } + return; + } + + if (isFavoritesDroppableId(source.droppableId)) { + if (isDefined(destination)) { + const dropTargetId = + getFavoritesDropTargetIdFromDestination(destination); + setActiveDropTargetId(dropTargetId); + } else { + setActiveDropTargetId(null); + } + } + }; + + const handleDragEnd = (result: DropResult, provided: ResponderProvided) => { + const isAddToNavigationSource = + result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; + const effectiveResult: DropResult = + isAddToNavigationSource && + !result.destination && + addToNavigationFallbackDestination + ? { ...result, destination: addToNavigationFallbackDestination } + : result; + + setIsDragging(false); + setSourceDroppableId(null); + setActiveDropTargetId(null); + setForbiddenDropTargetId(null); + setAddToNavigationFallbackDestination(null); + + if (isAddToNavigationSource) { + handleAddToNavigationDrop(effectiveResult, provided); + return; + } + + if (isFavoritesDroppableId(result.source.droppableId)) { + handleNavigationMenuItemDragAndDrop(result, provided); + return; + } + + handleFavoriteDragAndDrop(result, provided); + }; + + return ( + + + + + + {children} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx index 5508e98d5a..2a67bc5097 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerScrollableItems.tsx @@ -1,9 +1,11 @@ import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection'; +import { NavigationDrawerWorkspaceSectionSkeletonLoader } from '@/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader'; import { RemoteNavigationDrawerSection } from '@/object-metadata/components/RemoteNavigationDrawerSection'; + +import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection'; import { styled } from '@linaria/react'; import { lazy, Suspense } from 'react'; -import { NavigationDrawerOtherSection } from '@/navigation/components/NavigationDrawerOtherSection'; import { themeCssVariables } from 'twenty-ui/theme-constants'; const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = lazy(() => @@ -32,10 +34,8 @@ export const MainNavigationDrawerScrollableItems = () => { return ( - + }> - - diff --git a/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx b/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx index a28626b668..3f0169fc51 100644 --- a/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx +++ b/packages/twenty-front/src/modules/navigation/components/NavigationDrawerOtherSection.tsx @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { IconHelpCircle, IconSettings } from 'twenty-ui/display'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl'; @@ -54,24 +55,29 @@ export const NavigationDrawerOtherSection = () => { - {isNavigationSectionOpen && ( - <> - - - - )} + + + + ); }; diff --git a/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx b/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx index 081bb7224b..ab8208b796 100644 --- a/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx +++ b/packages/twenty-front/src/modules/navigation/components/PageDragDropProvider.tsx @@ -1,31 +1,12 @@ -import { - DragDropContext, - type DragStart, - type DropResult, - type OnDragUpdateResponder, - type ResponderProvided, -} from '@hello-pangea/dnd'; -import { type ReactNode, useCallback, useState } from 'react'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { lazy, Suspense, useState, type ReactNode } from 'react'; -import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext'; -import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop'; -import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; -import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; -import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext'; -import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext'; -import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop'; -import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleNavigationMenuItemDragAndDrop'; -import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop'; -import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState'; -import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState'; -import { getDropTargetIdFromDestination } from '@/navigation-menu-item/utils/getDropTargetIdFromDestination'; -import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId'; -import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId'; -import { useStore } from 'jotai'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { isDefined } from 'twenty-shared/utils'; +import { PageDragDropProviderMountEffect } from '@/navigation/components/PageDragDropProviderMountEffect'; + +const LazyWorkspaceDndKitProvider = lazy(() => + import('@/navigation/components/WorkspaceDndKitProvider').then((m) => ({ + default: m.WorkspaceDndKitProvider, + })), +); type PageDragDropProviderProps = { children: ReactNode; @@ -34,144 +15,22 @@ type PageDragDropProviderProps = { export const PageDragDropProvider = ({ children, }: PageDragDropProviderProps) => { - const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, - ); - const [isDragging, setIsDragging] = useState(false); - const [sourceDroppableId, setSourceDroppableId] = useState( - null, - ); - const [activeDropTargetId, setActiveDropTargetId] = useState( - null, - ); - const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< - string | null - >(null); - const [ - addToNavigationFallbackDestination, - setAddToNavigationFallbackDestination, - ] = useState<{ droppableId: string; index: number } | null>(null); + const [hasProviderMounted, setHasProviderMounted] = useState(false); - const store = useStore(); - const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState(); - const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop(); - const { handleNavigationMenuItemDragAndDrop } = - useHandleNavigationMenuItemDragAndDrop(); - const { handleWorkspaceNavigationMenuItemDragAndDrop } = - useHandleWorkspaceNavigationMenuItemDragAndDrop(); - const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop(); - - const orphanItemCount = workspaceNavigationMenuItems.filter( - (item) => !isDefined(item.folderId), - ).length; - - const handleDragStart = (dragStart: DragStart) => { - setIsDragging(true); - setSourceDroppableId(dragStart.source.droppableId); - if (dragStart.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - const defaultDestination = { - droppableId: - NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS, - index: orphanItemCount, - }; - setAddToNavigationFallbackDestination(defaultDestination); - setActiveDropTargetId(getDropTargetIdFromDestination(defaultDestination)); - } - }; - - const handleDragUpdate = useCallback( - ((update: Parameters[0]) => { - const { source, destination } = update; - if (source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID) { - return; - } - if ( - destination !== null && - isWorkspaceDroppableId(destination.droppableId) - ) { - setAddToNavigationFallbackDestination(destination); - const dropTargetId = getDropTargetIdFromDestination(destination); - setActiveDropTargetId(dropTargetId); - - const payload = - store - .get(addToNavPayloadRegistryState.atom) - .get(update.draggableId) ?? null; - const folderId = validateAndExtractWorkspaceFolderId( - destination.droppableId, - ); - const isFolderOverFolder = - payload?.type === 'folder' && folderId !== null; - setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null); - } else { - setForbiddenDropTargetId(null); - const fallback = addToNavigationFallbackDestination; - setActiveDropTargetId( - fallback ? getDropTargetIdFromDestination(fallback) : null, - ); - } - }) as OnDragUpdateResponder, - [addToNavigationFallbackDestination, store], - ); - - const handleDragEnd = (result: DropResult, provided: ResponderProvided) => { - const isAddToNavigationSource = - result.source.droppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; - const effectiveResult: DropResult = - isAddToNavigationSource && - !result.destination && - addToNavigationFallbackDestination - ? { ...result, destination: addToNavigationFallbackDestination } - : result; - - setIsDragging(false); - setSourceDroppableId(null); - setActiveDropTargetId(null); - setForbiddenDropTargetId(null); - setAddToNavigationFallbackDestination(null); - - if (isAddToNavigationSource) { - handleAddToNavigationDrop(effectiveResult, provided); - return; - } - - if (isNavigationMenuItemEditingEnabled) { - const isWorkspaceDrop = - isWorkspaceDroppableId(result.source?.droppableId) && - isWorkspaceDroppableId(result.destination?.droppableId); - if (isWorkspaceDrop) { - handleWorkspaceNavigationMenuItemDragAndDrop(result, provided); - } else { - handleNavigationMenuItemDragAndDrop(result, provided); - } - } else { - handleFavoriteDragAndDrop(result, provided); - } - }; + if (!hasProviderMounted) { + return ( + <> + setHasProviderMounted(true)} + /> + {children} + + ); + } return ( - - - - - - {children} - - - - - + {children}}> + {children} + ); }; diff --git a/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx b/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx new file mode 100644 index 0000000000..87a2aa9e71 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/PageDragDropProviderMountEffect.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; + +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; + +type PageDragDropProviderMountEffectProps = { + onEnterEditMode: () => void; +}; + +export const PageDragDropProviderMountEffect = ({ + onEnterEditMode, +}: PageDragDropProviderMountEffectProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + + useEffect(() => { + if (isNavigationMenuInEditMode) { + onEnterEditMode(); + } + }, [isNavigationMenuInEditMode, onEnterEditMode]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx b/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx new file mode 100644 index 0000000000..434f29a87d --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/components/WorkspaceDndKitProvider.tsx @@ -0,0 +1,50 @@ +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/contexts/NavigationDragSourceContext'; +import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; +import { NavigationMenuItemDragContext } from '@/navigation-menu-item/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 ( + + + + + sensors={WORKSPACE_DND_SENSORS} + onDragStart={handlers.onDragStart} + onDragOver={handlers.onDragOver} + onDragEnd={handlers.onDragEnd} + > + {children} + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts b/packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts new file mode 100644 index 0000000000..6b51bc126f --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/constants/workspaceDndKitDropResultOptions.ts @@ -0,0 +1,6 @@ +export const DROP_RESULT_OPTIONS = { + reason: 'DROP' as const, + combine: null, + mode: 'FLUID' as const, + type: 'DEFAULT' as const, +}; diff --git a/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts b/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts new file mode 100644 index 0000000000..b3931c3d2e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useWorkspaceDndKit.ts @@ -0,0 +1,351 @@ +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 { useStore } from 'jotai'; +import { isDefined } from 'twenty-shared/utils'; + +import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/hooks/useHandleAddToNavigationDrop'; +import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop'; +import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState'; +import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; +import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId'; +import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/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>['onDragStart'] + > +>[0]; +type DragOverPayload = Parameters< + NonNullable< + ComponentProps>['onDragOver'] + > +>[0]; +type DragEndPayload = Parameters< + NonNullable< + ComponentProps>['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( + null, + ); + const [activeDropTargetId, setActiveDropTargetId] = useState( + null, + ); + const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState< + string | null + >(null); + const [ + addToNavigationFallbackDestination, + setAddToNavigationFallbackDestination, + ] = useState(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, + ) => { + 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 }, + 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) { + applyWorkspaceReorderIfAllowed( + draggableId, + { droppableId: initialGroupStr, index: initialIndex }, + resolved.destination, + ); + 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, + }, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts b/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts new file mode 100644 index 0000000000..c2b638941a --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/preloadWorkspaceDndKit.ts @@ -0,0 +1,16 @@ +let preloadScheduled = false; + +const preload = () => { + void import('@/navigation/components/WorkspaceDndKitProvider'); + void import( + '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit' + ); +}; + +export const preloadWorkspaceDndKit = (): void => { + if (preloadScheduled) { + return; + } + preloadScheduled = true; + preload(); +}; diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts new file mode 100644 index 0000000000..97ba4b2e7e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDraggableData.ts @@ -0,0 +1,4 @@ +export type DraggableData = { + sourceDroppableId?: string; + sourceIndex?: number; +}; diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts new file mode 100644 index 0000000000..06a6f88be5 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDropDestination.ts @@ -0,0 +1 @@ +export type DropDestination = { droppableId: string; index: number }; diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts new file mode 100644 index 0000000000..3facdbf799 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitDroppableData.ts @@ -0,0 +1,4 @@ +export type DroppableData = { + droppableId: string; + index: number; +}; diff --git a/packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts new file mode 100644 index 0000000000..17b7d104f8 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/types/workspaceDndKitSortableTargetDestination.ts @@ -0,0 +1,8 @@ +import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; + +export type SortableTargetDestination = { + destination: DropDestination; + effectiveDropTargetId: string; + isTargetFolder: boolean; + dropTargetId: string; +}; diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts new file mode 100644 index 0000000000..2e1b9bf5ee --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget.ts @@ -0,0 +1,53 @@ +import { isDefined } from 'twenty-shared/utils'; +import type { NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId'; +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; + +import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; +import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination'; + +type GetNavItemById = ( + id: string | undefined, +) => NavigationMenuItem | undefined; + +export const getDestinationFromSortableTarget = ( + target: { id: unknown; group?: unknown; index?: unknown }, + getNavItemById: GetNavItemById, +): SortableTargetDestination | null => { + const group = target.group; + const rawIndex = target.index; + if (!isDefined(group) || !isDefined(rawIndex)) { + return null; + } + const index = Number(rawIndex); + if (!Number.isInteger(index) || index < 0) { + return null; + } + const destDroppableId = String(group); + const targetItem = getNavItemById( + target.id != null ? String(target.id) : undefined, + ); + const isTargetFolder = + isDefined(targetItem) && isNavigationMenuItemFolder(targetItem); + const dropTargetId = getDndKitDropTargetId(destDroppableId, index); + const effectiveDropTargetId = isTargetFolder + ? getDndKitDropTargetId( + `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`, + 0, + ) + : dropTargetId; + const destination: DropDestination = { + droppableId: isTargetFolder + ? `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}` + : destDroppableId, + index: isTargetFolder ? 0 : index, + }; + return { + destination, + effectiveDropTargetId, + isTargetFolder, + dropTargetId, + }; +}; diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts new file mode 100644 index 0000000000..a36b4d0cc7 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitIsFolderDrag.ts @@ -0,0 +1,13 @@ +import type { NavigationMenuItem } from '~/generated-metadata/graphql'; +import { isDefined } from 'twenty-shared/utils'; + +import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder'; + +type AddToNavPayload = { type?: string } | null; + +export const isFolderDrag = ( + payload: AddToNavPayload, + sourceItem: NavigationMenuItem | undefined, +): boolean => + payload?.type === 'folder' || + (isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem)); diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts new file mode 100644 index 0000000000..0ffc6b6e16 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitResolveDropTarget.ts @@ -0,0 +1,50 @@ +import { isDefined } from 'twenty-shared/utils'; +import type { NavigationMenuItem } from '~/generated-metadata/graphql'; + +import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId'; + +import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData'; +import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination'; +import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget'; + +type GetNavItemById = ( + id: string | undefined, +) => NavigationMenuItem | undefined; + +const isDroppableData = (data: unknown): data is DroppableData => + typeof data === 'object' && + data !== null && + typeof (data as DroppableData).droppableId === 'string' && + typeof (data as DroppableData).index === 'number'; + +export const resolveDropTarget = ( + target: { + id?: unknown; + group?: unknown; + index?: unknown; + data?: unknown; + } | null, + getNavItemById: GetNavItemById, +): SortableTargetDestination | null => { + if (target === null || target === undefined) { + return null; + } + if (isDefined(target.group) && isDefined(target.index)) { + return getDestinationFromSortableTarget( + { id: target.id, group: target.group, index: target.index }, + getNavItemById, + ); + } + if (isDroppableData(target.data)) { + const { droppableId, index } = target.data; + if (isWorkspaceDroppableId(droppableId)) { + return { + destination: { droppableId, index }, + effectiveDropTargetId: String(target.id), + isTargetFolder: false, + dropTargetId: String(target.id), + }; + } + } + return null; +}; diff --git a/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts new file mode 100644 index 0000000000..30e951e043 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/utils/workspaceDndKitToDropResult.ts @@ -0,0 +1,20 @@ +import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination'; +import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData'; + +export const toDropResult = ( + draggableId: string, + data: DraggableData | undefined, + destination: DropDestination | null, +): { + source: DropDestination; + destination: DropDestination | null; + draggableId: string; +} => { + const sourceDroppableId = data?.sourceDroppableId ?? ''; + const sourceIndex = data?.sourceIndex ?? 0; + return { + source: { droppableId: sourceDroppableId, index: sourceIndex }, + destination, + draggableId, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/EditModeProps.ts b/packages/twenty-front/src/modules/object-metadata/components/EditModeProps.ts new file mode 100644 index 0000000000..88de7bf163 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/EditModeProps.ts @@ -0,0 +1,4 @@ +export type EditModeProps = { + isSelectedInEditMode: boolean; + onEditModeClick?: () => void; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx index b3f169837b..b1b022d6d1 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -3,6 +3,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/ import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState'; @@ -28,7 +29,6 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export type NavigationDrawerItemForObjectMetadataItemProps = { objectMetadataItem: ObjectMetadataItem; navigationMenuItem?: ProcessedNavigationMenuItem; - isEditMode?: boolean; isSelectedInEditMode?: boolean; onEditModeClick?: () => void; onActiveItemClickWhenNotInEditMode?: () => void; @@ -38,12 +38,14 @@ export type NavigationDrawerItemForObjectMetadataItemProps = { export const NavigationDrawerItemForObjectMetadataItem = ({ objectMetadataItem, navigationMenuItem, - isEditMode = false, isSelectedInEditMode = false, onEditModeClick, - onActiveItemClickWhenNotInEditMode, + onActiveItemClickWhenNotInEditMode: _onActiveItemClickWhenNotInEditMode, isDragging = false, }: NavigationDrawerItemForObjectMetadataItemProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, ); @@ -99,18 +101,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ }) + '/', ); - const shouldUseClickHandler = isEditMode - ? Boolean(onEditModeClick) - : isActive && Boolean(onActiveItemClickWhenNotInEditMode); + const handleClick = isNavigationMenuInEditMode ? onEditModeClick : undefined; - const handleClick = shouldUseClickHandler - ? isEditMode - ? onEditModeClick - : onActiveItemClickWhenNotInEditMode - : undefined; - - const shouldNavigate = - !isEditMode && !(isActive && onActiveItemClickWhenNotInEditMode); + const shouldNavigate = !isNavigationMenuInEditMode; const isViewWithCustomName = isView && @@ -218,7 +211,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ label={label} secondaryLabel={secondaryLabel} to={ - isEditMode || isDragging + isNavigationMenuInEditMode || isDragging ? undefined : shouldNavigate ? navigationPath @@ -230,7 +223,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({ active={isActive} isSelectedInEditMode={isSelectedInEditMode} isDragging={isDragging} - triggerEvent={isEditMode ? 'CLICK' : undefined} + triggerEvent={isNavigationMenuInEditMode ? 'CLICK' : undefined} /> ); }; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx index 6d50c487e2..3e243f1422 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx @@ -3,10 +3,12 @@ import { useParams } from 'react-router-dom'; import { useWorkspaceFavorites } from '@/favorites/hooks/useWorkspaceFavorites'; import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/hooks/useWorkspaceNavigationMenuItems'; import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; -import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; +import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useLingui } from '@lingui/react/macro'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; @@ -24,14 +26,22 @@ export const NavigationDrawerOpenedSection = () => { const filteredActiveNonSystemObjectMetadataItems = activeObjectMetadataItems.filter((item) => !item.isRemote); - const loading = useIsPrefetchLoading(); + const isPrefetchLoading = useIsPrefetchLoading(); + const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, + ); + const prefetchIsLoaded = useAtomFamilyStateValue( + prefetchIsLoadedFamilyState, + PrefetchKey.AllNavigationMenuItems, + ); + + const loading = + isPrefetchLoading || + (isNavigationMenuItemEditingEnabled && !prefetchIsLoaded); const { workspaceFavoritesObjectMetadataItems } = useWorkspaceFavorites(); const { workspaceNavigationMenuItemsObjectMetadataItems } = useWorkspaceNavigationMenuItems(); - const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled( - FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED, - ); const { objectNamePlural: currentObjectNamePlural, @@ -67,7 +77,7 @@ export const NavigationDrawerOpenedSection = () => { .includes(objectMetadataItem.id); if (loading) { - return ; + return null; } return ( diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx index 4e996637e8..55be4ac770 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx @@ -28,7 +28,6 @@ type NavigationDrawerSectionForObjectMetadataItemsProps = { isRemote: boolean; objectMetadataItems: ObjectMetadataItem[]; rightIcon?: React.ReactNode; - isEditMode?: boolean; selectedObjectMetadataItemId?: string | null; onObjectMetadataItemClick?: (objectMetadataItem: ObjectMetadataItem) => void; onActiveObjectMetadataItemClick?: ( @@ -41,7 +40,6 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ isRemote, objectMetadataItems, rightIcon, - isEditMode = false, selectedObjectMetadataItemId = null, onObjectMetadataItemClick, onActiveObjectMetadataItemClick, @@ -119,6 +117,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ label={sectionTitle} onClick={() => toggleNavigationSection()} rightIcon={rightIcon} + isOpen={isNavigationSectionOpen} /> {isNavigationSectionOpen && @@ -127,7 +126,6 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ { + switch (item.itemType) { + case NavigationMenuItemType.FOLDER: + return ( + + ); + case NavigationMenuItemType.LINK: + return ( + + ); + default: + return ( + + ); + } +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent.tsx new file mode 100644 index 0000000000..e81acd73a8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent.tsx @@ -0,0 +1,72 @@ +import { lazy, Suspense } from 'react'; + +import { WorkspaceFolderReadOnly } from '@/object-metadata/components/WorkspaceFolderReadOnly'; +import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps'; + +const LazyWorkspaceNavigationMenuItemsFolder = lazy(() => + import( + '@/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder' + ).then((m) => ({ default: m.WorkspaceNavigationMenuItemsFolder })), +); + +type NavigationDrawerSectionForWorkspaceItemFolderContentProps = + WorkspaceSectionItemContentProps; + +export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({ + item, + editModeProps, + isDragging, + folderChildrenById, + folderCount, + selectedNavigationMenuItemId, + onNavigationMenuItemClick, + readOnly = false, +}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => { + const folderId = item.id; + const folderName = item.name ?? 'Folder'; + const folderIconKey = item.Icon; + const folderColor = 'color' in item ? item.color : undefined; + const navigationMenuItems = folderChildrenById.get(item.id) ?? []; + const isGroup = folderCount > 1; + + if (readOnly) { + return ( + + ); + } + return ( + + } + > + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent.tsx new file mode 100644 index 0000000000..1025e2441c --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent.tsx @@ -0,0 +1,45 @@ +import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { IconArrowUpRight } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +type NavigationDrawerSectionForWorkspaceItemLinkContentProps = + WorkspaceSectionItemContentProps; + +export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({ + item, + editModeProps, + isDragging, +}: NavigationDrawerSectionForWorkspaceItemLinkContentProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + const linkItem = item as ProcessedNavigationMenuItem; + return ( + } + active={false} + isSelectedInEditMode={editModeProps.isSelectedInEditMode} + isDragging={isDragging} + triggerEvent="CLICK" + rightOptions={ + !isNavigationMenuInEditMode && ( + + ) + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent.tsx new file mode 100644 index 0000000000..3400a3e224 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent.tsx @@ -0,0 +1,44 @@ +import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; +import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { coreViewsState } from '@/views/states/coreViewState'; +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; + +type NavigationDrawerSectionForWorkspaceItemObjectContentProps = + WorkspaceSectionItemContentProps; + +export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({ + item, + editModeProps, + isDragging, + onActiveObjectMetadataItemClick, +}: NavigationDrawerSectionForWorkspaceItemObjectContentProps) => { + const objectMetadataItems = useAtomStateValue(objectMetadataItemsState); + const coreViews = useAtomStateValue(coreViewsState); + const views = coreViews.map(convertCoreViewToView); + const objectMetadataItem = getObjectMetadataForNavigationMenuItem( + item as ProcessedNavigationMenuItem, + objectMetadataItems, + views, + ); + if (!objectMetadataItem) { + return null; + } + return ( + onActiveObjectMetadataItemClick(objectMetadataItem, item.id) + : undefined + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItems.tsx index dbc2a94fde..33763febff 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItems.tsx @@ -1,33 +1,25 @@ import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; -import { styled } from '@linaria/react'; -import { Droppable } from '@hello-pangea/dnd'; -import { useLingui } from '@lingui/react/macro'; -import { useContext } from 'react'; +import React, { lazy, Suspense, useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { IconLink, IconPlus } from 'twenty-ui/display'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; -import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget'; -import { WorkspaceNavigationMenuItemsFolder } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder'; import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; -import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants'; -import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext'; -import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection'; import { type FlatWorkspaceItem, type NavigationMenuItemClickParams, } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; -import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; +import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; +import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly'; +import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; -import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; 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'; @@ -35,18 +27,16 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState import { coreViewsState } from '@/views/states/coreViewState'; import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; -const StyledWorkspaceDroppableList = styled.div` - display: flex; - flex-direction: column; - gap: ${themeCssVariables.betweenSiblingsGap}; -`; +const LazyWorkspaceSectionListDndKit = lazy(() => + import( + '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit' + ).then((m) => ({ default: m.WorkspaceSectionListDndKit })), +); type NavigationDrawerSectionForWorkspaceItemsProps = { sectionTitle: string; items: FlatWorkspaceItem[]; rightIcon?: React.ReactNode; - onAddMenuItem?: () => void; - isEditMode?: boolean; selectedNavigationMenuItemId?: string | null; onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; onActiveObjectMetadataItemClick?: ( @@ -59,14 +49,13 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ sectionTitle, items, rightIcon, - onAddMenuItem, - isEditMode = false, selectedNavigationMenuItemId = null, onNavigationMenuItemClick, onActiveObjectMetadataItemClick, }: NavigationDrawerSectionForWorkspaceItemsProps) => { - const { t } = useLingui(); - const workspaceDropDisabled = useIsDropDisabledForSection(true); + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); const { toggleNavigationSection, isNavigationSectionOpen } = useNavigationSection('Workspace'); const coreViews = useAtomStateValue(coreViewsState); @@ -74,7 +63,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const objectMetadataItems = useAtomStateValue(objectMetadataItemsState); - const { isDragging } = useContext(NavigationMenuItemDragContext); const { addToNavigationFallbackDestination } = useContext( NavigationDropTargetContext, ); @@ -95,10 +83,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ return acc; }, new Map()); - const folderCount = flatItems.filter( - (item) => item.itemType === NavigationMenuItemType.FOLDER, - ).length; - const filteredItems = flatItems.filter((item) => { const type = item.itemType; if ( @@ -127,7 +111,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ return false; }); - const getEditModeProps = (item: FlatWorkspaceItem) => { + const getEditModeProps = (item: FlatWorkspaceItem): EditModeProps => { const itemId = item.id; return { isSelectedInEditMode: selectedNavigationMenuItemId === itemId, @@ -151,9 +135,6 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ }; }; - const isAddMenuItemButtonVisible = - isEditMode && isDefined(onAddMenuItem) && !isDragging; - if (flatItems.length === 0 && !isAddToNavigationDropTargetVisible) { return null; } @@ -165,193 +146,48 @@ export const NavigationDrawerSectionForWorkspaceItems = ({ label={sectionTitle} onClick={() => toggleNavigationSection()} rightIcon={rightIcon} - alwaysShowRightIcon={isEditMode} + alwaysShowRightIcon={isNavigationMenuInEditMode} + isOpen={isNavigationSectionOpen} /> - {(isNavigationSectionOpen || isAddToNavigationDropTargetVisible) && ( - - {(provided) => ( - - {filteredItems.map((item, index) => { - const type = item.itemType; - const editModeProps = getEditModeProps(item); - - if (type === 'folder') { - return ( - - 1} - isEditMode={isEditMode} - isSelectedInEditMode={ - editModeProps.isSelectedInEditMode - } - onEditModeClick={editModeProps.onEditModeClick} - onNavigationMenuItemClick={ - onNavigationMenuItemClick - } - selectedNavigationMenuItemId={ - selectedNavigationMenuItemId - } - isDragging={isDragging} - /> - } - /> - - ); + + {isNavigationMenuInEditMode ? ( + - - } - /> - - ); - } - - const objectMetadataItem = - getObjectMetadataForNavigationMenuItem( - item as ProcessedNavigationMenuItem, - objectMetadataItems, - views, - ); - if (!objectMetadataItem) return null; - - return ( - - - onActiveObjectMetadataItemClick( - objectMetadataItem, - item.id, - ) - : undefined - } - /> - } - /> - - ); - })} - - {isAddMenuItemButtonVisible && ( - - )} - - {addToNavigationFallbackDestination?.droppableId === - NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS && - addToNavigationFallbackDestination.index > - filteredItems.length && ( - - )} - {provided.placeholder} - - )} - - )} + /> + } + > + + + ) : ( + + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx new file mode 100644 index 0000000000..bcf8d5a550 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit.tsx @@ -0,0 +1,109 @@ +import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot'; +import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem'; +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext'; +import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext'; +import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { styled } from '@linaria/react'; +import { useContext } from 'react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent'; +import { WorkspaceOrphanDropTarget } from '@/object-metadata/components/WorkspaceOrphanDropTarget'; +import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton'; +import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps'; + +const StyledList = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.betweenSiblingsGap}; +`; + +const StyledListItemRow = styled.div` + display: flex; + flex-direction: column; + gap: 0; +`; + +export const WorkspaceSectionListDndKit = ({ + filteredItems, + getEditModeProps, + folderChildrenById, + selectedNavigationMenuItemId, + onNavigationMenuItemClick, + onActiveObjectMetadataItemClick, +}: WorkspaceSectionListDndKitProps) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + const workspaceDropDisabled = useIsDropDisabledForSection(true); + const { isDragging } = useContext(NavigationMenuItemDragContext); + const { addToNavigationFallbackDestination } = useContext( + NavigationDropTargetContext, + ); + const folderCount = filteredItems.filter( + (item) => item.itemType === NavigationMenuItemType.FOLDER, + ).length; + const isAddMenuItemButtonVisible = isNavigationMenuInEditMode; + return ( + + {filteredItems.map((item, index) => ( + + + + + + + ))} + + + {isAddMenuItemButtonVisible && } + + + {addToNavigationFallbackDestination?.droppableId === + NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS && + addToNavigationFallbackDestination.index > filteredItems.length && ( + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx new file mode 100644 index 0000000000..53e9156c43 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly.tsx @@ -0,0 +1,51 @@ +import { styled } from '@linaria/react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import type { FlatWorkspaceItem } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; +import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps'; +import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent'; + +const StyledList = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.betweenSiblingsGap}; +`; + +type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick< + WorkspaceSectionListDndKitProps, + 'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick' +>; + +const READ_ONLY_EDIT_MODE_PROPS = { + isSelectedInEditMode: false, + onEditModeClick: undefined, +} as const; + +export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({ + filteredItems, + folderChildrenById, + onActiveObjectMetadataItemClick, +}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => { + const folderCount = filteredItems.filter( + (item) => item.itemType === NavigationMenuItemType.FOLDER, + ).length; + + return ( + + {filteredItems.map((item: FlatWorkspaceItem) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader.tsx new file mode 100644 index 0000000000..9c3073a4b7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerWorkspaceSectionSkeletonLoader.tsx @@ -0,0 +1,53 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; +import { styled } from '@linaria/react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledTitleSkeleton = styled.div` + align-items: center; + display: flex; + height: ${themeCssVariables.spacing[5]}; + padding-left: ${themeCssVariables.spacing[1]}; + padding-right: ${themeCssVariables.spacing['0.5']}; +`; + +const StyledRowsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[1]}; + padding-left: ${themeCssVariables.spacing[1]}; +`; + +export const NavigationDrawerWorkspaceSectionSkeletonLoader = () => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceFolderReadOnly.tsx b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceFolderReadOnly.tsx new file mode 100644 index 0000000000..ea4d0ba121 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceFolderReadOnly.tsx @@ -0,0 +1,120 @@ +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 { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem'; +import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault'; +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder'; +import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer'; + +const StyledFolderContainer = styled.div` + border: 1px solid transparent; + border-radius: ${themeCssVariables.border.radius.sm}; +`; + +const StyledFolderContent = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledFolderExpandableWrapper = styled.div` + & > div { + overflow: visible !important; + } +`; + +type WorkspaceFolderReadOnlyProps = { + folderId: string; + folderName: string; + folderIconKey?: string | null; + folderColor?: string | null; + navigationMenuItems: ProcessedNavigationMenuItem[]; + isGroup: boolean; +}; + +export const WorkspaceFolderReadOnly = ({ + folderId, + folderName, + folderIconKey, + folderColor, + navigationMenuItems, + isGroup, +}: WorkspaceFolderReadOnlyProps) => { + const { getIcon } = useIcons(); + const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT); + const isMobile = useIsMobile(); + const { isOpen, handleToggle, selectedNavigationMenuItemIndex } = + useWorkspaceFolderOpenState({ folderId, navigationMenuItems }); + + const [skipInitialExpandAnimation] = useState(() => isOpen); + + return ( + + + = 0} + onClick={handleToggle} + className="navigation-drawer-item" + triggerEvent="CLICK" + preventCollapseOnMobile={isMobile} + alwaysShowRightOptions + rightOptions={ + isOpen ? ( + + ) : ( + + ) + } + /> + + + + {navigationMenuItems.map((navigationMenuItem, index) => ( + + ))} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceOrphanDropTarget.tsx b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceOrphanDropTarget.tsx new file mode 100644 index 0000000000..d543e9f781 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceOrphanDropTarget.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from 'react'; + +import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget'; +import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants'; +import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds'; +import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId'; + +type WorkspaceOrphanDropTargetProps = { + index: number; + compact?: boolean; + children?: ReactNode; +}; + +export const WorkspaceOrphanDropTarget = ({ + index, + compact = false, + children, +}: WorkspaceOrphanDropTargetProps) => ( + + {children} + +); diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionAddMenuItemButton.tsx b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionAddMenuItemButton.tsx new file mode 100644 index 0000000000..0776e19885 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionAddMenuItemButton.tsx @@ -0,0 +1,55 @@ +import { useLingui } from '@lingui/react/macro'; +import React from 'react'; +import { SidePanelPages } from 'twenty-shared/types'; +import { IconColumnInsertRight, IconPlus } from 'twenty-ui/display'; + +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; +import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; +import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel'; +import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; + +export const WorkspaceSectionAddMenuItemButton = () => { + const { t } = useLingui(); + const { navigateSidePanel } = useNavigateSidePanel(); + const sidePanelPage = useAtomStateValue(sidePanelPageState); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); + const setAddMenuItemInsertionContext = useSetAtomState( + addMenuItemInsertionContextState, + ); + + const setSelectedNavigationMenuItemInEditMode = useSetAtomState( + selectedNavigationMenuItemInEditModeState, + ); + + const handleClick = (event?: React.MouseEvent) => { + event?.stopPropagation(); + setAddMenuItemInsertionContext(null); + setSelectedNavigationMenuItemInEditMode(null); + navigateSidePanel({ + page: SidePanelPages.NavigationMenuAddItem, + pageTitle: t`New sidebar item`, + pageIcon: IconColumnInsertRight, + resetNavigationStack: true, + }); + }; + + const isSelected = + sidePanelPage === SidePanelPages.NavigationMenuAddItem && + addMenuItemInsertionContext === null; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionItemContentProps.ts b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionItemContentProps.ts new file mode 100644 index 0000000000..063962c589 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionItemContentProps.ts @@ -0,0 +1,23 @@ +import type { + FlatWorkspaceItem, + NavigationMenuItemClickParams, +} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; + +export type WorkspaceSectionItemContentProps = { + item: FlatWorkspaceItem; + editModeProps: EditModeProps; + isDragging: boolean; + folderChildrenById: Map; + folderCount: number; + selectedNavigationMenuItemId: string | null; + onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; + onActiveObjectMetadataItemClick?: ( + objectMetadataItem: ObjectMetadataItem, + navigationMenuItemId: string, + ) => void; + readOnly?: boolean; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListDndKitProps.ts b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListDndKitProps.ts new file mode 100644 index 0000000000..32d3a54bda --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListDndKitProps.ts @@ -0,0 +1,20 @@ +import type { + FlatWorkspaceItem, + NavigationMenuItemClickParams, +} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; +import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; +import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +import type { EditModeProps } from '@/object-metadata/components/EditModeProps'; + +export type WorkspaceSectionListDndKitProps = { + filteredItems: FlatWorkspaceItem[]; + getEditModeProps: (item: FlatWorkspaceItem) => EditModeProps; + folderChildrenById: Map; + selectedNavigationMenuItemId: string | null; + onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void; + onActiveObjectMetadataItemClick?: ( + objectMetadataItem: ObjectMetadataItem, + navigationMenuItemId: string, + ) => void; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx new file mode 100644 index 0000000000..ac45104de9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/WorkspaceSectionListEditModeFallback.tsx @@ -0,0 +1,32 @@ +import { styled } from '@linaria/react'; + +import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly'; +import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton'; +import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +const StyledFallback = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[1]}; +`; + +type WorkspaceSectionListEditModeFallbackProps = Pick< + WorkspaceSectionListDndKitProps, + 'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick' +>; + +export const WorkspaceSectionListEditModeFallback = ({ + filteredItems, + folderChildrenById, + onActiveObjectMetadataItemClick, +}: WorkspaceSectionListEditModeFallbackProps) => ( + + + + +); diff --git a/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts index c18541c3a5..826d8dba77 100644 --- a/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/read-only/hooks/useIsRecordReadOnly.ts @@ -1,8 +1,10 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly'; import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; type UseIsRecordReadOnlyParams = { recordId: string; @@ -13,6 +15,10 @@ export const useIsRecordReadOnly = ({ recordId, objectMetadataId, }: UseIsRecordReadOnlyParams) => { + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + const { objectMetadataItem } = useObjectMetadataItemById({ objectId: objectMetadataId, }); @@ -26,9 +32,12 @@ export const useIsRecordReadOnly = ({ const isRecordDeleted = useIsRecordDeleted({ recordId }); - return isRecordReadOnly({ - objectPermissions, - isRecordDeleted, - objectMetadataItem, - }); + return ( + isNavigationMenuInEditMode || + isRecordReadOnly({ + objectPermissions, + isRecordDeleted, + objectMetadataItem, + }) + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index e477d94917..90a93c05ae 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -1,3 +1,4 @@ +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; @@ -5,6 +6,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte import { isRecordTableCreateDisabled } from '@/object-record/record-table/utils/isRecordTableCreateDisabled'; import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { styled } from '@linaria/react'; import { type IconComponent } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; @@ -53,10 +55,15 @@ export const RecordTableEmptyStateDisplay = ( const objectPermissions = useObjectPermissionsForObject( objectMetadataItem.id, ); - const isReadOnly = isObjectMetadataReadOnly({ - objectPermissions, - objectMetadataItem, - }); + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); + const isReadOnly = + isNavigationMenuInEditMode || + isObjectMetadataReadOnly({ + objectPermissions, + objectMetadataItem, + }); const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue( hasAnySoftDeleteFilterOnViewComponentSelector, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx index a8f7f8decb..3394d8b919 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton.tsx @@ -1,9 +1,11 @@ +import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { isRecordTableCreateDisabled } from '@/object-record/record-table/utils/isRecordTableCreateDisabled'; import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { styled } from '@linaria/react'; import { IconPlus } from 'twenty-ui/display'; import { themeCssVariables } from 'twenty-ui/theme-constants'; @@ -20,6 +22,9 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => { useRecordTableContextOrThrow(); const isMobile = useIsMobile(); + const isNavigationMenuInEditMode = useAtomStateValue( + isNavigationMenuInEditModeState, + ); const { createNewIndexRecord } = useCreateNewIndexRecord({ objectMetadataItem, @@ -31,10 +36,12 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => { }); }; - const isReadOnly = isObjectMetadataReadOnly({ - objectPermissions, - objectMetadataItem, - }); + const isReadOnly = + isNavigationMenuInEditMode || + isObjectMetadataReadOnly({ + objectPermissions, + objectMetadataItem, + }); const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue( hasAnySoftDeleteFilterOnViewComponentSelector, diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelAddToNavigationDroppable.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelAddToNavigationDroppable.tsx index 22c0006e57..720086f6a8 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelAddToNavigationDroppable.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelAddToNavigationDroppable.tsx @@ -1,11 +1,24 @@ -import { Droppable, type DroppableProvided } from '@hello-pangea/dnd'; -import { type ReactNode, useContext } from 'react'; +import { lazy, Suspense, useContext, type ReactNode } from 'react'; import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId'; import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext'; +import type { AddToNavDroppableProvided } from '@/command-menu/components/CommandMenuAddToNavDroppableTypes'; + +const FALLBACK_PROVIDED: AddToNavDroppableProvided = { + innerRef: () => {}, + droppableProps: {}, + placeholder: null, +}; + +const CommandMenuAddToNavDroppableDndKit = lazy(() => + import('@/command-menu/components/CommandMenuAddToNavDroppableDndKit').then( + (m) => ({ default: m.CommandMenuAddToNavDroppableDndKit }), + ), +); + type SidePanelAddToNavigationDroppableProps = { - children: (provided: DroppableProvided) => ReactNode; + children: (provided: AddToNavDroppableProvided) => ReactNode; }; export const SidePanelAddToNavigationDroppable = ({ @@ -15,11 +28,11 @@ export const SidePanelAddToNavigationDroppable = ({ const isDropDisabled = sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID; return ( - - {(provided) => children(provided)} - + + + ); }; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelItemWithAddToNavigationDrag.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelItemWithAddToNavigationDrag.tsx index 24d0303d50..ebc9514376 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelItemWithAddToNavigationDrag.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelItemWithAddToNavigationDrag.tsx @@ -1,23 +1,22 @@ import { styled } from '@linaria/react'; -import { Draggable } from '@hello-pangea/dnd'; import { useLingui } from '@lingui/react/macro'; -import { type ReactNode, useState } from 'react'; +import { type ReactNode, lazy, Suspense, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { type IconComponent } from 'twenty-ui/display'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { AddToNavigationDragHandle } from '@/navigation-menu-item/components/AddToNavigationDragHandle'; import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; -const StyledDraggableMenuItem = styled.div` - cursor: grab; - width: 100%; - - &:active { - cursor: grabbing; - } -`; +const CommandMenuItemWithAddToNavigationDragDndKit = lazy(() => + import( + '@/command-menu/components/CommandMenuItemWithAddToNavigationDragDndKit' + ).then((m) => ({ + default: m.CommandMenuItemWithAddToNavigationDragDndKit, + })), +); type SidePanelItemWithAddToNavigationDragProps = { icon?: IconComponent; @@ -28,8 +27,25 @@ type SidePanelItemWithAddToNavigationDragProps = { onClick: () => void; payload: AddToNavigationDragPayload; dragIndex?: number; + disabled?: boolean; + disableDrag?: boolean; }; +const StyledDraggableMenuItem = styled.div<{ + $disabled?: boolean; + $disableDrag?: boolean; +}>` + cursor: ${({ $disabled, $disableDrag }) => + $disabled || $disableDrag ? 'default' : 'grab'}; + pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')}; + width: 100%; + + &:active { + cursor: ${({ $disabled, $disableDrag }) => + $disabled || $disableDrag ? 'default' : 'grabbing'}; + } +`; + export const SidePanelItemWithAddToNavigationDrag = ({ icon, customIconContent, @@ -39,6 +55,8 @@ export const SidePanelItemWithAddToNavigationDrag = ({ onClick, payload, dragIndex, + disabled = false, + disableDrag = false, }: SidePanelItemWithAddToNavigationDragProps) => { const { t } = useLingui(); const setAddToNavPayloadRegistry = useSetAtomState( @@ -46,7 +64,8 @@ export const SidePanelItemWithAddToNavigationDrag = ({ ); const [isHovered, setIsHovered] = useState(false); - const contextualDescription = isHovered + const showDragAffordance = !disabled && !disableDrag && isHovered; + const contextualDescription = showDragAffordance ? t`Drag to add to navbar` : description; @@ -55,23 +74,31 @@ export const SidePanelItemWithAddToNavigationDrag = ({ icon={icon} customIconContent={customIconContent} payload={payload} - isHovered={isHovered} + isHovered={showDragAffordance} + disabled={disabled} + disableDrag={disableDrag} /> ); const registerPayload = () => { - if (dragIndex !== undefined) { + if (!disabled && !disableDrag && isDefined(dragIndex)) { setAddToNavPayloadRegistry((prev) => new Map(prev).set(id, payload)); } }; const menuItemContent = ( { - setIsHovered(true); - registerPayload(); + if (!disabled && !disableDrag) { + setIsHovered(true); + registerPayload(); + } + }} + onMouseLeave={() => { + if (!disabled && !disableDrag) setIsHovered(false); }} - onMouseLeave={() => setIsHovered(false)} onMouseDown={registerPayload} > ); - if (dragIndex !== undefined) { - return ( - - {(provided) => ( -
- {menuItemContent} -
- )} -
- ); + if (!isDefined(dragIndex) || disableDrag) { + return menuItemContent; } - return menuItemContent; + return ( + + + + ); }; diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx index e356762056..05e0ff00d1 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelLinkInfo.tsx @@ -1,14 +1,14 @@ import { useLingui } from '@lingui/react/macro'; -import { IconLink } from 'twenty-ui/display'; +import { IconLink, IconWorld } from 'twenty-ui/display'; -import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout'; -import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; -import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState'; -import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; +import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft'; import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems'; import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; +import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout'; +import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; +import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState'; import { TitleInput } from '@/ui/input/components/TitleInput'; import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -59,8 +59,10 @@ export const SidePanelLinkInfo = () => { return ( } diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx index 6213b104ee..10a2383e2b 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx @@ -8,7 +8,7 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { styled } from '@linaria/react'; import { motion } from 'framer-motion'; -import { useContext } from 'react'; +import React, { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { ThemeContext } from 'twenty-ui/theme-constants'; @@ -21,11 +21,16 @@ export const SidePanelRouter = () => { const sidePanelPage = useAtomStateValue(sidePanelPageState); const sidePanelPageInfo = useAtomStateValue(sidePanelPageInfoState); - const sidePanelPageComponent = isDefined(sidePanelPage) ? ( - SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage) - ) : ( - <> - ); + const rawPageComponent = isDefined(sidePanelPage) + ? SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage) + : null; + + const sidePanelPageComponent = + isDefined(rawPageComponent) && React.isValidElement(rawPageComponent) + ? React.cloneElement(rawPageComponent, { + key: sidePanelPageInfo.instanceId, + }) + : rawPageComponent; const { theme } = useContext(ThemeContext); diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView.tsx index a25484ca3b..de5c8b0196 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView.tsx @@ -3,22 +3,24 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; import { getAbsoluteUrl } from 'twenty-shared/utils'; +import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item'; +import { extractDomainFromUrl } from '@/navigation-menu-item/utils/extractDomainFromUrl'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; -import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption'; import { type OrganizeActionsProps, SidePanelEditOrganizeActions, } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions'; import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection'; import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds'; -import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item'; -import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor'; import { TextInput } from '@/ui/input/components/TextInput'; type SidePanelEditLinkItemViewProps = OrganizeActionsProps & { selectedItem: ProcessedNavigationMenuItem; - onUpdateLink: (linkId: string, link: string) => void; + onUpdateLink: ( + linkId: string, + updates: { link?: string; name?: string }, + ) => void; onOpenFolderPicker: () => void; }; @@ -36,28 +38,49 @@ export const SidePanelEditLinkItemView = ({ }: SidePanelEditLinkItemViewProps) => { const { t } = useLingui(); const [urlEditInput, setUrlEditInput] = useState(''); + const [lastAutoSetName, setLastAutoSetName] = useState(null); + const defaultLabel = t`Link label`; const selectableItemIds = getOrganizeActionsSelectableItemIds(true); + const currentName = selectedItem.name ?? defaultLabel; + const currentDomain = selectedItem.link + ? extractDomainFromUrl(getAbsoluteUrl(selectedItem.link)) + : undefined; + const canAutoUpdateName = + currentName === defaultLabel || + currentName === currentDomain || + currentName === lastAutoSetName; + + const handleUrlChange = (value: string) => { + setUrlEditInput(value); + if (!canAutoUpdateName) return; + const trimmed = value.trim(); + if (!isNonEmptyString(trimmed)) return; + const domain = extractDomainFromUrl(getAbsoluteUrl(trimmed)); + if (domain !== undefined) { + setLastAutoSetName(domain); + onUpdateLink(selectedItem.id, { name: domain }); + } + }; + + const handleUrlBlur = (event: React.FocusEvent) => { + const value = event.target.value.trim(); + if (isNonEmptyString(value)) { + onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) }); + setUrlEditInput(''); + } + }; + return ( - setUrlEditInput(value)} - onBlur={(event) => { - const value = event.target.value.trim(); - if (isNonEmptyString(value)) { - onUpdateLink(selectedItem.id, getAbsoluteUrl(value)); - setUrlEditInput(''); - } - }} + onChange={handleUrlChange} + onBlur={handleUrlBlur} /> { updateLinkInDraft(linkId, { link })} + onUpdateLink={(linkId, updates) => + updateLinkInDraft(linkId, updates) + } onOpenFolderPicker={openFolderPicker} canMoveUp={canMoveUp} canMoveDown={canMoveDown} diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu.tsx index c3e5033cb6..219772a310 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu.tsx @@ -1,4 +1,5 @@ import { useLingui } from '@lingui/react/macro'; +import { isDefined } from 'twenty-shared/utils'; import { Avatar, IconBuildingSkyscraper, @@ -6,20 +7,20 @@ import { IconLink, IconTable, } from 'twenty-ui/display'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; -import { SidePanelAddToNavigationDraggablePlaceholder } from '@/side-panel/components/SidePanelAddToNavigationDraggablePlaceholder'; -import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; +import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; +import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; +import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { useAddFolderToNavigationMenu } from '@/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu'; import { useAddLinkToNavigationMenu } from '@/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu'; -import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; -import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; -import { useContext } from 'react'; -import { ThemeContext } from 'twenty-ui/theme-constants'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; type SidePanelNewSidebarItemMainMenuProps = { onSelectObject: () => void; @@ -27,104 +28,133 @@ type SidePanelNewSidebarItemMainMenuProps = { onSelectRecord: () => void; }; +const MAIN_MENU_ITEM_TYPES = [ + NavigationMenuItemType.OBJECT, + NavigationMenuItemType.VIEW, + NavigationMenuItemType.RECORD, + NavigationMenuItemType.FOLDER, + NavigationMenuItemType.LINK, +] as const; + export const SidePanelNewSidebarItemMainMenu = ({ onSelectObject, onSelectView, onSelectRecord, }: SidePanelNewSidebarItemMainMenuProps) => { const { t } = useLingui(); - const { theme } = useContext(ThemeContext); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); const { handleAddFolder } = useAddFolderToNavigationMenu(); const { handleAddLink } = useAddLinkToNavigationMenu(); + const isAddingToFolder = isDefined( + addMenuItemInsertionContext?.targetFolderId, + ); + const isDragDisabled = addMenuItemInsertionContext?.disableDrag === true; + const selectableItemIds = isAddingToFolder + ? MAIN_MENU_ITEM_TYPES.filter( + (type) => type !== NavigationMenuItemType.FOLDER, + ) + : [...MAIN_MENU_ITEM_TYPES]; + return ( {({ innerRef, droppableProps, placeholder }) => ( - - {/* oxlint-disable-next-line react/jsx-props-no-spreading */} + + {/* eslint-disable-next-line react/jsx-props-no-spreading */}
- - - ( - - )} - label={t`Object`} - id="object" - hasSubMenu={true} - onClick={onSelectObject} - /> - - - - - ( - - )} - label={t`View`} - id="view" - hasSubMenu={true} - onClick={onSelectView} - /> - - - - - ( - - )} - label={t`Record`} - id="record" - hasSubMenu={true} - onClick={onSelectRecord} - /> - - + + ( + + )} + label={t`Object`} + id={NavigationMenuItemType.OBJECT} + hasSubMenu={true} + onClick={onSelectObject} + /> + + + ( + + )} + label={t`View`} + id={NavigationMenuItemType.VIEW} + hasSubMenu={true} + onClick={onSelectView} + /> + + + ( + + )} + label={t`Record`} + id={NavigationMenuItemType.RECORD} + hasSubMenu={true} + onClick={onSelectRecord} + /> + - + - + diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectFlow.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectFlow.tsx index abd06beade..6202f2e809 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectFlow.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectFlow.tsx @@ -1,21 +1,22 @@ import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; -import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; -import { SidePanelObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView'; -import { SidePanelSystemObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView'; -import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem'; import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft'; -import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems'; import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView'; +import { SidePanelSystemObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView'; +import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { ViewKey } from '@/views/types/ViewKey'; +import { useIcons } from 'twenty-ui/display'; type SidePanelNewSidebarItemObjectFlowProps = { onBack: () => void; @@ -25,7 +26,7 @@ export const SidePanelNewSidebarItemObjectFlow = ({ onBack, }: SidePanelNewSidebarItemObjectFlowProps) => { const { t } = useLingui(); - const { closeSidePanelMenu } = useSidePanelMenu(); + const { getIcon } = useIcons(); const [objectSearchInput, setObjectSearchInput] = useState(''); const [systemObjectSearchInput, setSystemObjectSearchInput] = useState(''); const [isInSystemPicker, setIsInSystemPicker] = useState(false); @@ -33,6 +34,8 @@ export const SidePanelNewSidebarItemObjectFlow = ({ const { currentDraft } = useDraftNavigationMenuItems(); const { objectMetadataItems } = useObjectMetadataItems(); const { addObjectToDraft } = useAddObjectToNavigationMenuDraft(); + const { openNavigationMenuItemInSidePanel } = + useOpenNavigationMenuItemInSidePanel(); const { activeNonSystemObjectMetadataItems } = useFilteredObjectMetadataItems(); const addMenuItemInsertionContext = useAtomStateValue( @@ -68,7 +71,7 @@ export const SidePanelNewSidebarItemObjectFlow = ({ if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) { return; } - addObjectToDraft( + const itemId = addObjectToDraft( objectMetadataItem.id, defaultViewId, currentDraft, @@ -77,7 +80,11 @@ export const SidePanelNewSidebarItemObjectFlow = ({ getStandardObjectIconColor(objectMetadataItem.nameSingular), ); setAddMenuItemInsertionContext(null); - closeSidePanelMenu(); + openNavigationMenuItemInSidePanel({ + itemId, + pageTitle: objectMetadataItem.labelSingular, + pageIcon: getIcon(objectMetadataItem.icon), + }); }; const handleBackToObjectList = () => { @@ -85,6 +92,8 @@ export const SidePanelNewSidebarItemObjectFlow = ({ setSystemObjectSearchInput(''); }; + const disableDrag = addMenuItemInsertionContext?.disableDrag === true; + if (isInSystemPicker) { return ( ); } @@ -110,6 +120,7 @@ export const SidePanelNewSidebarItemObjectFlow = ({ isViewItem={false} onChangeObject={handleSelectObject} objectMenuItemVariant="add" + disableDrag={disableDrag} /> ); }; diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemPage.tsx index 220b168f9f..157e77aa5a 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemPage.tsx @@ -1,14 +1,21 @@ import { useState } from 'react'; +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; import { SidePanelNewSidebarItemMainMenu } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemMainMenu'; import { SidePanelNewSidebarItemObjectFlow } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemObjectFlow'; import { SidePanelNewSidebarItemRecordSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubView'; import { SidePanelNewSidebarItemViewFlow } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewFlow'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { isDefined } from 'twenty-shared/utils'; type SelectedOption = 'object' | 'record' | 'view' | null; export const SidePanelNewSidebarItemPage = () => { const [selectedOption, setSelectedOption] = useState(null); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); + const disableDrag = isDefined(addMenuItemInsertionContext); const handleBackToMain = () => { setSelectedOption(null); @@ -20,7 +27,12 @@ export const SidePanelNewSidebarItemPage = () => { case 'object': return ; case 'record': - return ; + return ( + + ); default: return ( { - const { closeSidePanelMenu } = useSidePanelMenu(); + const { getIcon } = useIcons(); const { addRecordToDraft } = useAddRecordToNavigationMenuDraft(); const { currentDraft } = useDraftNavigationMenuItems(); const addMenuItemInsertionContext = useAtomStateValue( @@ -38,6 +40,8 @@ export const SidePanelNewSidebarItemRecordItem = ({ const setAddMenuItemInsertionContext = useSetAtomState( addMenuItemInsertionContextState, ); + const { openNavigationMenuItemInSidePanel } = + useOpenNavigationMenuItemInSidePanel(); const { objectMetadataItems } = useObjectMetadataItems(); const objectMetadataItem = objectMetadataItems.find( (item) => item.nameSingular === record.objectNameSingular, @@ -52,7 +56,7 @@ export const SidePanelNewSidebarItemRecordItem = ({ }; const handleSelectRecord = () => { - addRecordToDraft( + const itemId = addRecordToDraft( { recordId: record.recordId, objectNameSingular: record.objectNameSingular, @@ -64,7 +68,13 @@ export const SidePanelNewSidebarItemRecordItem = ({ addMenuItemInsertionContext?.targetIndex, ); setAddMenuItemInsertionContext(null); - closeSidePanelMenu(); + openNavigationMenuItemInSidePanel({ + itemId, + pageTitle: record.label, + pageIcon: objectMetadataItem + ? getIcon(objectMetadataItem.icon) + : IconFolder, + }); }; return ( @@ -89,6 +99,7 @@ export const SidePanelNewSidebarItemRecordItem = ({ id={record.recordId} onClick={handleSelectRecord} dragIndex={dragIndex} + disableDrag={disableDrag} payload={recordPayload} /> diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubView.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubView.tsx index a80472bf76..9e3d872c8e 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubView.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordSubView.tsx @@ -3,17 +3,19 @@ import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useDebounce } from 'use-debounce'; -import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; -import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; -import { SidePanelList } from '@/side-panel/components/SidePanelList'; -import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults'; -import { SidePanelNewSidebarItemRecordItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordItem'; import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems'; +import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; +import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; +import { SidePanelList } from '@/side-panel/components/SidePanelList'; +import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; +import { SidePanelNewSidebarItemRecordItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemRecordItem'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSearchQuery } from '~/generated/graphql'; type SearchRecordBase = { @@ -25,12 +27,19 @@ type SearchRecordBase = { type SidePanelNewSidebarItemRecordSubViewProps = { onBack: () => void; + disableDrag?: boolean; }; export const SidePanelNewSidebarItemRecordSubView = ({ onBack, + disableDrag: disableDragProp, }: SidePanelNewSidebarItemRecordSubViewProps) => { const { t } = useLingui(); + const addMenuItemInsertionContext = useAtomStateValue( + addMenuItemInsertionContextState, + ); + const disableDrag = + disableDragProp ?? addMenuItemInsertionContext?.disableDrag === true; const { currentDraft } = useDraftNavigationMenuItems(); const { objectMetadataItems } = useObjectMetadataItems(); const [recordSearchInput, setRecordSearchInput] = useState(''); @@ -105,7 +114,8 @@ export const SidePanelNewSidebarItemRecordSubView = ({ ))} diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewFlow.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewFlow.tsx index bd6f2ce35d..3ff87d489d 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewFlow.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewFlow.tsx @@ -1,15 +1,16 @@ import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { SidePanelNewSidebarItemViewObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewObjectPickerSubView'; -import { SidePanelNewSidebarItemViewPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView'; -import { SidePanelNewSidebarItemViewSystemSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewSystemSubView'; -import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem'; import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems'; import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { SidePanelNewSidebarItemViewObjectPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewObjectPickerSubView'; +import { SidePanelNewSidebarItemViewPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView'; +import { SidePanelNewSidebarItemViewSystemSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewSystemSubView'; +import { getAvailableObjectMetadataForNewSidebarItem } from '@/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem'; import { ViewKey } from '@/views/types/ViewKey'; +import { ViewType } from '@/views/types/ViewType'; type SidePanelNewSidebarItemViewFlowProps = { onBack: () => void; @@ -33,7 +34,10 @@ export const SidePanelNewSidebarItemViewFlow = ({ const objectMetadataIdsWithDisplayableViews = new Set( views - .filter((view) => view.key !== ViewKey.Index) + .filter( + (view) => + view.key !== ViewKey.Index && view.type !== ViewType.FieldsWidget, + ) .map((view) => view.objectMetadataId), ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView.tsx index ef0c262deb..1e96bb8a5e 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelNewSidebarItemViewPickerSubView.tsx @@ -1,27 +1,29 @@ -import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; -import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; -import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; -import { SidePanelList } from '@/side-panel/components/SidePanelList'; -import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; -import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; -import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems'; import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft'; import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems'; import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft'; +import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; +import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; +import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; +import { SidePanelList } from '@/side-panel/components/SidePanelList'; +import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; +import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { type View } from '@/views/types/View'; import { ViewKey } from '@/views/types/ViewKey'; +import { ViewType } from '@/views/types/ViewType'; import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; +import { ViewVisibility } from '~/generated-metadata/graphql'; type SidePanelNewSidebarItemViewPickerSubViewProps = { selectedObjectMetadataIdForView: string; @@ -35,7 +37,6 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ const { t } = useLingui(); const { getIcon } = useIcons(); const [searchValue, setSearchValue] = useState(''); - const { closeSidePanelMenu } = useSidePanelMenu(); const { addViewToDraft } = useAddViewToNavigationMenuDraft(); const { currentDraft } = useDraftNavigationMenuItems(); const addMenuItemInsertionContext = useAtomStateValue( @@ -44,6 +45,8 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ const setAddMenuItemInsertionContext = useSetAtomState( addMenuItemInsertionContextState, ); + const { openNavigationMenuItemInSidePanel } = + useOpenNavigationMenuItemInSidePanel(); const { objectMetadataItems } = useObjectMetadataItems(); const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft); @@ -51,7 +54,9 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ .filter( (view) => view.objectMetadataId === selectedObjectMetadataIdForView && - view.key !== ViewKey.Index, + view.key !== ViewKey.Index && + view.type !== ViewType.FieldsWidget && + view.visibility === ViewVisibility.WORKSPACE, ) .sort((a, b) => a.position - b.position); @@ -75,8 +80,10 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ ? t`No results found` : t`No custom views available`; + const isDragDisabled = addMenuItemInsertionContext?.disableDrag === true; + const handleSelectView = (view: View) => { - addViewToDraft( + const itemId = addViewToDraft( view.id, currentDraft, addMenuItemInsertionContext?.targetFolderId ?? null, @@ -86,7 +93,11 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ : undefined, ); setAddMenuItemInsertionContext(null); - closeSidePanelMenu(); + openNavigationMenuItemInSidePanel({ + itemId, + pageTitle: view.name, + pageIcon: getIcon(view.icon), + }); }; return ( @@ -136,7 +147,8 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({ label={view.name} id={view.id} onClick={() => handleSelectView(view)} - dragIndex={index} + dragIndex={isDragDisabled ? undefined : index} + disableDrag={isDragDisabled} payload={{ type: NavigationMenuItemType.VIEW, viewId: view.id, diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem.tsx index 63bc9fa79e..6d7a7fbcf3 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem.tsx @@ -3,13 +3,13 @@ import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; -import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems'; import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft'; import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector'; @@ -22,6 +22,7 @@ type SidePanelObjectMenuItemProps = { ) => void; variant: 'add' | 'edit'; dragIndex?: number; + disableDrag?: boolean; }; export const SidePanelObjectMenuItem = ({ @@ -29,6 +30,7 @@ export const SidePanelObjectMenuItem = ({ onSelect, variant, dragIndex, + disableDrag = false, }: SidePanelObjectMenuItemProps) => { const { t } = useLingui(); const { getIcon } = useIcons(); @@ -66,11 +68,13 @@ export const SidePanelObjectMenuItem = ({ id={objectMetadataItem.id} onClick={handleClick} dragIndex={dragIndex} + disableDrag={disableDrag} payload={{ type: NavigationMenuItemType.OBJECT, objectMetadataId: objectMetadataItem.id, defaultViewId: defaultViewId ?? '', label: objectMetadataItem.labelPlural, + iconColor, }} /> ) : ( diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem.tsx index 5b2fe22653..51affb8150 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem.tsx @@ -2,10 +2,10 @@ import { isDefined } from 'twenty-shared/utils'; import { useIcons } from 'twenty-ui/display'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; -import { SidePanelObjectMenuItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem'; import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelObjectMenuItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectMenuItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; type SidePanelObjectPickerItemProps = { @@ -18,6 +18,7 @@ type SidePanelObjectPickerItemProps = { ) => void; objectMenuItemVariant?: 'add' | 'edit'; dragIndex?: number; + disableDrag?: boolean; }; export const SidePanelObjectPickerItem = ({ @@ -27,6 +28,7 @@ export const SidePanelObjectPickerItem = ({ onChangeObject, objectMenuItemVariant = 'edit', dragIndex, + disableDrag = false, }: SidePanelObjectPickerItemProps) => { const { getIcon } = useIcons(); @@ -59,6 +61,7 @@ export const SidePanelObjectPickerItem = ({ onSelect={onChangeObject} variant={objectMenuItemVariant} dragIndex={dragIndex} + disableDrag={disableDrag} /> ); }; diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView.tsx index 7ea1394cdb..6a5ed4f0c4 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerSubView.tsx @@ -1,16 +1,15 @@ import { useLingui } from '@lingui/react/macro'; import { IconSettings } from 'twenty-ui/display'; -import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; -import { SidePanelAddToNavigationDraggablePlaceholder } from '@/side-panel/components/SidePanelAddToNavigationDraggablePlaceholder'; -import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; +import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; +import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems'; import { SidePanelObjectPickerItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem'; -import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; type SidePanelObjectPickerSubViewProps = { @@ -27,6 +26,7 @@ type SidePanelObjectPickerSubViewProps = { ) => void; objectMenuItemVariant?: 'add' | 'edit'; emptyNoResultsText?: string; + disableDrag?: boolean; }; export const SidePanelObjectPickerSubView = ({ @@ -40,6 +40,7 @@ export const SidePanelObjectPickerSubView = ({ onChangeObject, objectMenuItemVariant = 'edit', emptyNoResultsText, + disableDrag = false, }: SidePanelObjectPickerSubViewProps) => { const { t } = useLingui(); const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } = @@ -66,34 +67,19 @@ export const SidePanelObjectPickerSubView = ({ onSelectObjectForViewEdit={onSelectObjectForViewEdit} onChangeObject={onChangeObject} objectMenuItemVariant={objectMenuItemVariant} - dragIndex={isAddVariant ? index : undefined} + dragIndex={isAddVariant && !disableDrag ? index : undefined} + disableDrag={disableDrag} /> ))} - {isAddVariant ? ( - - - } - label={t`System objects`} - id="system" - hasSubMenu - onClick={onOpenSystemPicker} - /> - - - ) : ( - - } - label={t`System objects`} - id="system" - hasSubMenu - onClick={onOpenSystemPicker} - /> - - )} + + } + label={t`System objects`} + id="system" + hasSubMenu + onClick={onOpenSystemPicker} + /> + ); diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView.tsx b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView.tsx index 38ee11ef8a..6d57a9c572 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/components/SidePanelSystemObjectPickerSubView.tsx @@ -1,11 +1,12 @@ import { useLingui } from '@lingui/react/macro'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable'; import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup'; import { SidePanelList } from '@/side-panel/components/SidePanelList'; -import { SidePanelObjectPickerItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem'; import { SidePanelSubViewWithSearch } from '@/side-panel/components/SidePanelSubViewWithSearch'; import { useSidePanelFilteredPickerItems } from '@/side-panel/hooks/useSidePanelFilteredPickerItems'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SidePanelObjectPickerItem } from '@/side-panel/pages/navigation-menu-item/components/SidePanelObjectPickerItem'; type SidePanelSystemObjectPickerSubViewProps = { systemObjects: ObjectMetadataItem[]; @@ -20,6 +21,7 @@ type SidePanelSystemObjectPickerSubViewProps = { ) => void; objectMenuItemVariant?: 'add' | 'edit'; emptyNoResultsText?: string; + disableDrag?: boolean; }; export const SidePanelSystemObjectPickerSubView = ({ @@ -32,6 +34,7 @@ export const SidePanelSystemObjectPickerSubView = ({ onChangeObject, objectMenuItemVariant = 'edit', emptyNoResultsText, + disableDrag = false, }: SidePanelSystemObjectPickerSubViewProps) => { const { t } = useLingui(); const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } = @@ -45,6 +48,25 @@ export const SidePanelSystemObjectPickerSubView = ({ ? t`No results found` : (emptyNoResultsText ?? t`No system objects available`); + const isAddVariant = objectMenuItemVariant === 'add'; + + const listContent = ( + + {filteredItems.map((objectMetadataItem, index) => ( + + ))} + + ); + return ( - - - {filteredItems.map((objectMetadataItem) => ( - - ))} - - + {isAddVariant ? ( + + {({ innerRef, droppableProps, placeholder }) => ( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} +
+ {listContent} + {placeholder} +
+
+ )} +
+ ) : ( + + {listContent} + + )}
); }; diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu.ts b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu.ts index 1078718cf3..58f758615c 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu.ts +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddFolderToNavigationMenu.ts @@ -7,7 +7,6 @@ import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/u import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; -import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; @@ -18,9 +17,6 @@ export const useAddFolderToNavigationMenu = () => { const navigationMenuItemsDraft = useAtomStateValue( navigationMenuItemsDraftState, ); - const setSelectedNavigationMenuItemInEditMode = useSetAtomState( - selectedNavigationMenuItemInEditModeState, - ); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const addMenuItemInsertionContext = useAtomStateValue( @@ -46,8 +42,8 @@ export const useAddFolderToNavigationMenu = () => { ); setAddMenuItemInsertionContext(null); - setSelectedNavigationMenuItemInEditMode(itemId); openNavigationMenuItemInSidePanel({ + itemId, pageTitle: t`Edit folder`, pageIcon: IconFolder, focusTitleInput: true, diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu.ts b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu.ts index f8ac4f5deb..48f0e51b40 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu.ts +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useAddLinkToNavigationMenu.ts @@ -7,7 +7,6 @@ import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/u import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel'; import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState'; import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState'; -import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; @@ -18,9 +17,6 @@ export const useAddLinkToNavigationMenu = () => { const navigationMenuItemsDraft = useAtomStateValue( navigationMenuItemsDraftState, ); - const setSelectedNavigationMenuItemInEditMode = useSetAtomState( - selectedNavigationMenuItemInEditModeState, - ); const { openNavigationMenuItemInSidePanel } = useOpenNavigationMenuItemInSidePanel(); const addMenuItemInsertionContext = useAtomStateValue( @@ -47,8 +43,8 @@ export const useAddLinkToNavigationMenu = () => { ); setAddMenuItemInsertionContext(null); - setSelectedNavigationMenuItemInEditMode(itemId); openNavigationMenuItemInSidePanel({ + itemId, pageTitle: t`Edit link`, pageIcon: IconLink, focusTitleInput: true, diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions.ts b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions.ts index e77a68cec0..31fb1dce9c 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions.ts +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions.ts @@ -41,6 +41,7 @@ const getAddMenuItemInsertionContext = ( return { targetFolderId, targetIndex: selectedIndexInFolder + offset, + disableDrag: true, }; }; diff --git a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem.ts b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem.ts index 1ea7ef15b2..8121e0d317 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem.ts +++ b/packages/twenty-front/src/modules/side-panel/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem.ts @@ -20,9 +20,9 @@ export const getAvailableObjectMetadataForNewSidebarItem = ({ objectMetadataIdsWithIndexView, objectMetadataIdsWithDisplayableViews, }: GetAvailableObjectMetadataForNewSidebarItemProps): GetAvailableObjectMetadataForNewSidebarItemResult => { - const availableObjectMetadataItems = [ - ...activeNonSystemObjectMetadataItems, - ].sort((a, b) => a.labelPlural.localeCompare(b.labelPlural)); + const availableObjectMetadataItems = activeNonSystemObjectMetadataItems + .filter((item) => objectMetadataIdsWithIndexView.has(item.id)) + .sort((a, b) => a.labelPlural.localeCompare(b.labelPlural)); const activeSystemObjectMetadataItems = objectMetadataItems .filter((item) => item.isActive && item.isSystem) diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index f083b40dc6..541b9beb28 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -64,6 +64,7 @@ export type NavigationDrawerItemProps = { mouseUpNavigation?: boolean; preventCollapseOnMobile?: boolean; isSelectedInEditMode?: boolean; + variant?: 'default' | 'tertiary'; }; type StyledItemProps = Pick< @@ -75,6 +76,7 @@ type StyledItemProps = Pick< | 'to' | 'isDragging' | 'isSelectedInEditMode' + | 'variant' > & { isNavigationDrawerExpanded: boolean; hasRightOptions: boolean; @@ -93,7 +95,7 @@ const StyledItem = styled.button` : '1px solid transparent'}; border-radius: ${themeCssVariables.border.radius.sm}; box-sizing: border-box; - color: ${({ active, danger, soon }) => { + color: ${({ active, danger, soon, variant }) => { if (active === true) { return themeCssVariables.font.color.primary; } @@ -103,6 +105,9 @@ const StyledItem = styled.button` if (soon === true) { return themeCssVariables.font.color.light; } + if (variant === 'tertiary') { + return themeCssVariables.font.color.tertiary; + } return themeCssVariables.font.color.secondary; }}; cursor: ${({ soon, isDragging }) => @@ -113,7 +118,6 @@ const StyledItem = styled.button` height: ${themeCssVariables.spacing[7]}; margin-top: ${({ indentationLevel }) => indentationLevel === 2 ? '2px' : '0'}; - padding-bottom: ${themeCssVariables.spacing[1]}; padding-left: ${themeCssVariables.spacing[1]}; padding-right: ${({ hasRightOptions }) => @@ -121,12 +125,13 @@ const StyledItem = styled.button` ? themeCssVariables.spacing['0.5'] : themeCssVariables.spacing[1]}; padding-top: ${themeCssVariables.spacing[1]}; - pointer-events: ${({ soon }) => (soon ? 'none' : 'auto')}; - text-decoration: none; - user-select: none; + width: ${({ isNavigationDrawerExpanded, hasRightOptions }) => + !isNavigationDrawerExpanded + ? `calc(${NAVIGATION_DRAWER_COLLAPSED_WIDTH}px - ${themeCssVariables.spacing[6]} + ${themeCssVariables.spacing[1]} + ${hasRightOptions ? themeCssVariables.spacing['0.5'] : themeCssVariables.spacing[1]})` + : `calc(100% - ${themeCssVariables.spacing['1.5']} + ${themeCssVariables.spacing[1]} + ${hasRightOptions ? themeCssVariables.spacing['0.5'] : themeCssVariables.spacing[1]})`}; &:hover { background: ${themeCssVariables.background.transparent.light}; @@ -140,11 +145,6 @@ const StyledItem = styled.button` visibility: visible; } - width: ${({ isNavigationDrawerExpanded, hasRightOptions }) => - !isNavigationDrawerExpanded - ? `calc(${NAVIGATION_DRAWER_COLLAPSED_WIDTH}px - ${themeCssVariables.spacing[6]} + ${themeCssVariables.spacing[1]} + ${hasRightOptions ? themeCssVariables.spacing['0.5'] : themeCssVariables.spacing[1]})` - : `calc(100% - ${themeCssVariables.spacing['1.5']} + ${themeCssVariables.spacing[1]} + ${hasRightOptions ? themeCssVariables.spacing['0.5'] : themeCssVariables.spacing[1]})`}; - @media (max-width: ${MOBILE_VIEWPORT}px) { font-size: ${themeCssVariables.font.size.lg}; } @@ -294,6 +294,7 @@ export const NavigationDrawerItem = ({ mouseUpNavigation = false, preventCollapseOnMobile = false, isSelectedInEditMode = false, + variant = 'default', }: NavigationDrawerItemProps) => { const { theme } = useContext(ThemeContext); const isMobile = useIsMobile(); @@ -350,6 +351,7 @@ export const NavigationDrawerItem = ({ aria-selected={active} danger={danger} soon={soon} + variant={variant} as={ to ? isExternalLink @@ -452,12 +454,7 @@ export const NavigationDrawerItem = ({ {isDefined(rightOptions) && ( - { - e.stopPropagation(); - e.preventDefault(); - }} - > + { const isMobile = useIsMobile(); const isNavigationDrawerExpanded = useAtomStateValue( @@ -78,10 +96,21 @@ export const NavigationDrawerSectionTitle = ({ return ; } + const ChevronIcon = isOpen === true ? IconChevronDown : IconChevronRight; + return ( - + + {isOpen !== undefined && ( + + + + )} {isDefined(rightIcon) && ( { return ( ); }; diff --git a/yarn.lock b/yarn.lock index 2d779f2be2..4a113328e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4090,6 +4090,76 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/abstract@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/abstract@npm:0.3.2" + dependencies: + "@dnd-kit/geometry": "npm:^0.3.2" + "@dnd-kit/state": "npm:^0.3.2" + tslib: "npm:^2.6.2" + checksum: 10c0/b4443f867576718791a57f10a5a422d06273753f4ef5b1025bda05c94aefee9928fbb8b845a5ceb2f481424fbc6c2c67fb7a2acc1f54a4d336a12b627bb53fb5 + languageName: node + linkType: hard + +"@dnd-kit/collision@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/collision@npm:0.3.2" + dependencies: + "@dnd-kit/abstract": "npm:^0.3.2" + "@dnd-kit/geometry": "npm:^0.3.2" + tslib: "npm:^2.6.2" + checksum: 10c0/dde6c6c3b692460402f3b60aaf45d6f13dd329bfdce825edd31a82b2193c46a032ea77dd96fc9342bd04cb0069dec1a88ae383086fb2979da317684cf0ea6687 + languageName: node + linkType: hard + +"@dnd-kit/dom@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/dom@npm:0.3.2" + dependencies: + "@dnd-kit/abstract": "npm:^0.3.2" + "@dnd-kit/collision": "npm:^0.3.2" + "@dnd-kit/geometry": "npm:^0.3.2" + "@dnd-kit/state": "npm:^0.3.2" + tslib: "npm:^2.6.2" + checksum: 10c0/4e759457b8bf8e76452574d5ef80dd3868e03d7b72ea6fac7c63a8f5c86ddc182e66300c6c46b8427159fa91becaa9039d980a1cc11f9a68f1dca54d2a9b1c50 + languageName: node + linkType: hard + +"@dnd-kit/geometry@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/geometry@npm:0.3.2" + dependencies: + "@dnd-kit/state": "npm:^0.3.2" + tslib: "npm:^2.6.2" + checksum: 10c0/eed63886251a3a81ceebb20b35538e274d357fc65015697285daa9503fa154b3525bee3159cc864a496c926f22c124b708335812416185134553baf62c266cea + languageName: node + linkType: hard + +"@dnd-kit/react@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/react@npm:0.3.2" + dependencies: + "@dnd-kit/abstract": "npm:^0.3.2" + "@dnd-kit/dom": "npm:^0.3.2" + "@dnd-kit/state": "npm:^0.3.2" + tslib: "npm:^2.6.2" + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 10c0/92a5b2f4005d9684a2a86efcfc56898f8e98a045dea85862858afb6406b08ba559e52dc1b151777d50577c7cb1aa040bf98c8410df7fda11862ebe128a68fd10 + languageName: node + linkType: hard + +"@dnd-kit/state@npm:^0.3.2": + version: 0.3.2 + resolution: "@dnd-kit/state@npm:0.3.2" + dependencies: + "@preact/signals-core": "npm:^1.10.0" + tslib: "npm:^2.6.2" + checksum: 10c0/9fcbb8a1e7924bfb5eaeb65a34fcd65bb37f739a85eaa303ae3562a1ef9aae4156c60baf397e4923cdb038b908ad18758fb42fcd6cde1ca45dace26d052c96fa + languageName: node + linkType: hard + "@docsearch/css@npm:3.6.2": version: 3.6.2 resolution: "@docsearch/css@npm:3.6.2" @@ -16288,6 +16358,13 @@ __metadata: languageName: node linkType: hard +"@preact/signals-core@npm:^1.10.0": + version: 1.13.0 + resolution: "@preact/signals-core@npm:1.13.0" + checksum: 10c0/25f536b90f5dbd116e037a27b51f5ca69914a957b5fa3c1d8af2857d145257fdb106e86cb6e1d5d22bc6feb1215d4448bf208482d2349f832720548bc788ca08 + languageName: node + linkType: hard + "@preact/signals-core@npm:^1.8.0": version: 1.12.2 resolution: "@preact/signals-core@npm:1.12.2" @@ -22953,6 +23030,13 @@ __metadata: languageName: node linkType: hard +"@trysound/sax@npm:0.2.0": + version: 0.2.0 + resolution: "@trysound/sax@npm:0.2.0" + checksum: 10c0/44907308549ce775a41c38a815f747009ac45929a45d642b836aa6b0a536e4978d30b8d7d680bbd116e9dd73b7dbe2ef0d1369dcfc2d09e83ba381e485ecbe12 + languageName: node + linkType: hard + "@ts-gql/tag@npm:^0.7.3": version: 0.7.3 resolution: "@ts-gql/tag@npm:0.7.3" @@ -29565,7 +29649,7 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^2.0.1, brace-expansion@npm:^2.0.2": +"brace-expansion@npm:^2.0.1": version: 2.0.2 resolution: "brace-expansion@npm:2.0.2" dependencies: @@ -45030,7 +45114,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:10.2.4, minimatch@npm:^10.1.1, minimatch@npm:^10.2.2": +"minimatch@npm:10.2.4, minimatch@npm:^10.2.2": version: 10.2.4 resolution: "minimatch@npm:10.2.4" dependencies: @@ -45039,7 +45123,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.1.2": +"minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -45066,48 +45150,48 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.5 - resolution: "minimatch@npm:3.1.5" +"minimatch@npm:^10.1.1": + version: 10.1.1 + resolution: "minimatch@npm:10.1.1" dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10c0/c85d44821c71973d636091fddbfbffe62370f5ee3caf0241c5b60c18cd289e916200acb2361b7e987558cd06896d153e25d505db9fc1e43e6b4b6752e2702902 languageName: node linkType: hard "minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": - version: 5.1.9 - resolution: "minimatch@npm:5.1.9" + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 10c0/4202718683815a7288b13e470160a4f9560cf392adef4f453927505817e01ef6b3476ecde13cfcaed17e7326dd3b69ad44eb2daeb19a217c5500f9277893f1d6 + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 languageName: node linkType: hard "minimatch@npm:^7.2.0": - version: 7.4.9 - resolution: "minimatch@npm:7.4.9" + version: 7.4.6 + resolution: "minimatch@npm:7.4.6" dependencies: - brace-expansion: "npm:^2.0.2" - checksum: 10c0/8d5406a9697edb9b7ea02697d58cabcb3d3a9a4a02caa1cf57b9ab5ae22c78b2945600661a78f91d1545f77521f97f3cb5f8cb066e58356a121b50e4e60ccdbe + brace-expansion: "npm:^2.0.1" + checksum: 10c0/e587bf3d90542555a3d58aca94c549b72d58b0a66545dd00eef808d0d66e5d9a163d3084da7f874e83ca8cc47e91c670e6c6f6593a3e7bb27fcc0e6512e87c67 languageName: node linkType: hard "minimatch@npm:^8.0.2": - version: 8.0.7 - resolution: "minimatch@npm:8.0.7" + version: 8.0.4 + resolution: "minimatch@npm:8.0.4" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 10c0/46d9dee24174f8a9eadec97ba36cba2e63f1fff8b36324e1825229bd9307ffee7ffd2f5a2749b29ba796eda877cd9c1687f9d1b399a10b290346561f2a8145f8 + checksum: 10c0/a0a394c356dd5b4cb7f821720841a82fa6f07c9c562c5b716909d1b6ec5e56a7e4c4b5029da26dd256b7d2b3a3f38cbf9ddd8680e887b9b5282b09c05501c1ca languageName: node linkType: hard "minimatch@npm:^9.0.0, minimatch@npm:^9.0.3, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": - version: 9.0.9 - resolution: "minimatch@npm:9.0.9" + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" dependencies: - brace-expansion: "npm:^2.0.2" - checksum: 10c0/0b6a58530dbb00361745aa6c8cffaba4c90f551afe7c734830bd95fd88ebf469dd7355a027824ea1d09e37181cfeb0a797fb17df60c15ac174303ac110eb7e86 + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed languageName: node linkType: hard @@ -52182,13 +52266,6 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.5.0": - version: 1.5.0 - resolution: "sax@npm:1.5.0" - checksum: 10c0/bc3b60a7bfecd40b18256596e96b32df2488339ae1e00a77f842b568f0831228a16c3bd357ec500241ec0b9dc7a475a1286427795c4a8c50bb8e8878f3435dd8 - languageName: node - linkType: hard - "saxes@npm:^6.0.0": version: 6.0.0 resolution: "saxes@npm:6.0.0" @@ -54483,36 +54560,36 @@ __metadata: linkType: hard "svgo@npm:^2.8.0": - version: 2.8.2 - resolution: "svgo@npm:2.8.2" + version: 2.8.0 + resolution: "svgo@npm:2.8.0" dependencies: + "@trysound/sax": "npm:0.2.0" commander: "npm:^7.2.0" css-select: "npm:^4.1.3" css-tree: "npm:^1.1.3" csso: "npm:^4.2.0" picocolors: "npm:^1.0.0" - sax: "npm:^1.5.0" stable: "npm:^0.1.8" bin: - svgo: ./bin/svgo - checksum: 10c0/a3a533e1678aecdfa1c67f06d71f104da7ef574a3f63a8dfeda10368b42428c67d09a06b4eee233c5ed49ac815f1febb6193cba0f611a21bfc00366d7930205d + svgo: bin/svgo + checksum: 10c0/0741f5d5cad63111a90a0ce7a1a5a9013f6d293e871b75efe39addb57f29a263e45294e485a4d2ff9cc260a5d142c8b5937b2234b4ef05efdd2706fb2d360ecc languageName: node linkType: hard "svgo@npm:^3.0.2": - version: 3.3.3 - resolution: "svgo@npm:3.3.3" + version: 3.3.2 + resolution: "svgo@npm:3.3.2" dependencies: + "@trysound/sax": "npm:0.2.0" commander: "npm:^7.2.0" css-select: "npm:^5.1.0" css-tree: "npm:^2.3.1" css-what: "npm:^6.1.0" csso: "npm:^5.0.5" picocolors: "npm:^1.0.0" - sax: "npm:^1.5.0" bin: svgo: ./bin/svgo - checksum: 10c0/06568c6b0430f96748c557f0b17dc7de79b19fa16d13d7523527ede0ec727fc6d8e6a10e13ff106dc4372d2e6063a1dca7c455c495efb1b83857480425f9b965 + checksum: 10c0/a6badbd3d1d6dbb177f872787699ab34320b990d12e20798ecae915f0008796a0f3c69164f1485c9def399e0ce0a5683eb4a8045e51a5e1c364bb13a0d9f79e1 languageName: node linkType: hard @@ -55677,6 +55754,7 @@ __metadata: "@calcom/embed-react": "npm:^1.5.3" "@cyntler/react-doc-viewer": "npm:^1.17.0" "@dagrejs/dagre": "npm:^1.1.8" + "@dnd-kit/react": "npm:^0.3.2" "@floating-ui/react": "npm:^0.24.3" "@graphiql/plugin-explorer": "npm:^1.0.2" "@graphiql/react": "npm:^0.23.0"