Compare commits

...

123 Commits

Author SHA1 Message Date
Abdul Rahman 54e9cc60db Refactor navigation components to utilize AnimatedExpandableContainer for smoother transitions
Updated multiple navigation components, including CurrentWorkspaceMemberFavoritesFolders, NavigationDrawerOtherSection, CurrentWorkspaceMemberNavigationMenuItemFolders, and NavigationDrawerSectionForWorkspaceItems, to replace conditional rendering with AnimatedExpandableContainer. This change enhances the user experience by providing animated transitions for expanded sections, improving visual feedback and interaction consistency across the navigation UI.
2026-03-06 09:31:23 +05:30
Abdul Rahman 36b2c31cab Add rightOptions to NavigationDrawerSectionForWorkspaceItemLinkContent for enhanced UI
Introduced a new rightOptions prop in the NavigationDrawerSectionForWorkspaceItemLinkContent component, which conditionally renders an IconArrowUpRight when not in edit mode. This enhancement improves the visual feedback and interactivity of the navigation drawer, aligning with the overall user experience improvements in the application.
2026-03-06 09:22:57 +05:30
Abdul Rahman 1c6525e8ee Enhance WorkspaceNavigationMenuItemsFolder and WorkspaceFolderReadOnly components with active state management
Added active state handling to the WorkspaceNavigationMenuItemsFolder and WorkspaceFolderReadOnly components, ensuring proper visual feedback based on the selected navigation menu item index. Refactored the click handling for the folder toggle to improve user interaction and maintain consistent behavior across components.
2026-03-06 09:21:30 +05:30
Abdul Rahman db9c0b85e4 Implement animated transitions for NavigationMenuEditModeBar visibility
Refactored the NavigationMenuEditModeBar component to utilize framer-motion for smooth animations when toggling the visibility of the edit mode bar. This enhancement improves user experience by providing a visually appealing transition effect, ensuring that the layout customization options are presented more dynamically.
2026-03-06 09:13:42 +05:30
Abdul Rahman 39ad4d3d2f Remove navigation side panel invocation from handleEditClick in WorkspaceNavigationMenuItems component 2026-03-06 09:10:51 +05:30
Abdul Rahman 03a044de27 Add extractDomainFromUrl utility and enhance SidePanelEditLinkItemView for domain extraction
Introduced a new utility function, extractDomainFromUrl, to extract the domain from a given URL. Updated the SidePanelEditLinkItemView component to utilize this function for automatically setting the link name based on the URL input. Refactored the onUpdateLink method to accept an object for updates, improving flexibility in link management.
2026-03-06 09:09:47 +05:30
Abdul Rahman 94310cfbbd Adjust positioning of StyledViewOverlay in ObjectIconWithViewOverlay component for improved layout consistency. Updated bottom, right, and width properties to enhance visual alignment. 2026-03-06 09:00:42 +05:30
Abdul Rahman 54b01c4526 Enhance AddMenuItemInsertionContext and update drag handling logic
Added an optional disableDrag property to the AddMenuItemInsertionContext type to control drag behavior. Updated various components to utilize this new property, ensuring that drag functionality is correctly managed based on the insertion context. This change improves the user experience by providing more granular control over item interactions in the navigation menu.
2026-03-06 08:58:55 +05:30
Abdul Rahman 9c9cf04b5c Enhance WorkspaceNavigationMenuItemsFolder and related hooks for improved state management
Updated the WorkspaceNavigationMenuItemsFolder component to integrate side panel state management, allowing for better handling of menu item insertion context and selected navigation items in edit mode. Refactored the useOpenAddItemToFolderPage hook to reset the selected item in edit mode when opening the add item page. Additionally, modified the WorkspaceSectionAddMenuItemButton to ensure proper state handling when navigating to the add item side panel, improving overall user experience and code clarity.
2026-03-06 08:50:46 +05:30
Abdul Rahman 4fe3642dae Refactor navigation menu item hooks to streamline state management
Removed the selectedNavigationMenuItemInEditModeState from useHandleAddToNavigationDrop and updated related logic to directly handle itemId in openNavigationMenuItemInSidePanel. Enhanced the useOpenNavigationMenuItemInSidePanel hook to set the selected item in edit mode when opening the side panel. This refactor improves code clarity and reduces unnecessary state management, contributing to a more efficient navigation experience.
2026-03-06 08:42:13 +05:30
Abdul Rahman 4ed20b18d1 Refactor NavigationDrawerItem color logic for improved clarity
Updated the color logic in the NavigationDrawerItem component to simplify the conditional rendering. The changes enhance code readability and maintainability by removing unnecessary conditions, ensuring a more straightforward approach to determining the icon color based on the component's state.
2026-03-06 08:32:32 +05:30
Abdul Rahman f061abfb48 Refactor SidePanelRouter to improve component rendering logic
Updated the SidePanelRouter component to enhance the rendering logic by introducing a check for valid React elements before cloning the page component. This change ensures that the component handles rendering more robustly, improving maintainability and user experience.
2026-03-06 08:30:44 +05:30
Abdul Rahman 2ba06f1392 Enhance NavigationDrawerSectionTitle styling for improved hover effect
Updated the NavigationDrawerSectionTitle component to apply a new hover style that changes the color of the section title label. This enhancement improves the visual feedback for users interacting with the navigation drawer, contributing to a more engaging user experience.
2026-03-06 08:26:28 +05:30
Abdul Rahman e9880d7593 Refactor CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher for improved rendering logic
Updated the CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher component to enhance the rendering logic by incorporating the AnimatedEaseInOut component. This change allows for smoother transitions between the editing and viewing states of the navigation menu items, improving the overall user experience. The conditional rendering now effectively toggles between CurrentWorkspaceMemberNavigationMenuItemFolders and CurrentWorkspaceMemberFavoritesFolders based on the editing state.
2026-03-06 08:23:59 +05:30
Abdul Rahman 9c492bdf28 Update LinkIconWithLinkOverlay styles for improved layout consistency
Modified the StyledLinkOverlay component to use theme variables for border radius and adjusted positioning properties for better alignment. Increased the size of the LinkIcon for enhanced visibility, contributing to a more cohesive design within the navigation menu.
2026-03-06 08:20:10 +05:30
Abdul Rahman 9fc38ef10c Refactor SidePanelEditLinkItemView to remove unused color option component
Removed the SidePanelEditColorOption component from the SidePanelEditLinkItemView, streamlining the code and enhancing maintainability. Updated imports accordingly to reflect this change.
2026-03-06 08:01:07 +05:30
Abdul Rahman c63703ae36 Add DroppableData type and refactor resolveDropTarget for improved data handling
Introduced a new DroppableData type to encapsulate droppableId and index properties. Updated the resolveDropTarget function to utilize this type for better data validation and handling. Additionally, removed the parseDropTargetIdToDestination utility as its functionality is now integrated into the resolveDropTarget logic, streamlining the code and enhancing maintainability.
2026-03-06 03:52:36 +05:30
Abdul Rahman 0b9d8d9469 Refactor resolveDropTarget function for improved readability
Updated the resolveDropTarget function to enhance code clarity by adding braces around the null check condition. This change improves the readability of the function and maintains consistency with coding standards.
2026-03-06 03:36:59 +05:30
Abdul Rahman 62527d77f2 Refactor workspaceDndKitResolveDropTarget to improve import organization
Updated the workspaceDndKitResolveDropTarget file to reorganize imports for better clarity and maintainability. The NavigationMenuItem type import was moved to the top, and unnecessary comments were removed, streamlining the code structure.
2026-03-06 03:36:48 +05:30
Abdul Rahman 09d2325ebf Refactor getDestinationFromSortableTarget to improve index handling
Updated the getDestinationFromSortableTarget function to enhance the handling of the index parameter. The raw index is now validated to ensure it is an integer and non-negative before being used. This change improves the robustness of the drag-and-drop functionality within the navigation system.
2026-03-06 03:31:47 +05:30
Abdul Rahman 7694969b11 Refactor CommandMenuNewSidebarItemPage to utilize side panel components
Updated the CommandMenuNewSidebarItemPage component to replace command menu imports and usage with corresponding side panel components. This change enhances consistency in navigation and aligns with recent refactoring efforts to streamline the user experience across the application.
2026-03-06 03:25:59 +05:30
Abdul Rahman f162a76f16 Refactor navigation and object metadata components to use side panel navigation
Updated various components to replace command menu navigation with side panel navigation. This includes changes in `WorkspaceNavigationMenuItems`, `WorkspaceSectionAddMenuItemButton`, and related components. Additionally, imports have been adjusted to utilize `themeCssVariables` for consistent theming across the application. These changes enhance the user experience by streamlining navigation and improving code maintainability.
2026-03-06 01:24:06 +05:30
Abdul Rahman b54bcd6799 Update package.json to add @dnd-kit/react dependency 2026-03-06 01:19:15 +05:30
Abdul Rahman 07ccee62bd Refactor WorkspaceDndKitProvider and related files for improved drag-and-drop functionality
Updated the WorkspaceDndKitProvider component to streamline drag-and-drop logic by removing unused imports and state management. Introduced new utility files for handling draggable data, drop destinations, and resolving drop targets. This refactor enhances code organization and maintainability, while also improving the overall drag-and-drop experience within the workspace navigation.
2026-03-06 01:19:06 +05:30
Abdul Rahman 8e31c0da3c Refactor usePersistField to remove navigation menu edit mode check
Updated the usePersistField hook to eliminate the check for isNavigationMenuInEditModeState. This change simplifies the logic within the hook, enhancing readability and maintainability by focusing solely on the field persistence functionality.
2026-03-06 00:35:15 +05:30
Abdul Rahman 3b38681f03 Refactor NavigationDrawerSectionForWorkspaceItemContent to use specific components for folders, links, and objects
Updated the NavigationDrawerSectionForWorkspaceItemContent component to delegate rendering to specialized components: NavigationDrawerSectionForWorkspaceItemFolderContent, NavigationDrawerSectionForWorkspaceItemLinkContent, and NavigationDrawerSectionForWorkspaceItemObjectContent. This change improves code organization and readability by separating concerns for different item types. Additionally, updated imports in related components to reflect the new structure.
2026-03-06 00:31:04 +05:30
Abdul Rahman 9b83cbdfc0 Refactor SidePanelNewSidebarItemMainMenu to use NavigationMenuItemType constants
Updated the SidePanelNewSidebarItemMainMenu component to replace string literals with constants from NavigationMenuItemType for improved type safety and maintainability. This change enhances code readability and consistency across the component.
2026-03-06 00:23:03 +05:30
Abdul Rahman 2bad7198e9 Refactor SidePanelItemWithAddToNavigationDrag to simplify contextual description logic
Updated the logic for determining the contextual description in SidePanelItemWithAddToNavigationDrag. The new implementation improves readability by consolidating the conditions for displaying the drag affordance message.
2026-03-06 00:18:56 +05:30
Abdul Rahman 811a5ffbd1 Refactor DefaultRecordActionsConfig to simplify soft delete condition
Updated the logic in DEFAULT_RECORD_ACTIONS_CONFIG to streamline the condition for soft delete availability. The check for `isNavigationMenuInEditMode` has been moved to the end of the condition, improving readability and maintaining functionality.
2026-03-06 00:14:13 +05:30
Abdul Rahman 785c96ac53 Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-06 00:11:03 +05:30
Abdul Rahman c260c9e5e7 refactor: migrate styled components from Emotion to Linaria
Updated multiple components to replace the Emotion styled import with Linaria's styled import. This change enhances performance and aligns with the project's styling strategy, ensuring consistency across the codebase.
2026-03-05 19:45:29 +05:30
Abdul Rahman 4558513c6b feat: add Edit Navigation Sidebar action to record actions configuration
Introduced a new action for editing the navigation sidebar in the record actions configuration. This includes the addition of the EditNavigationSidebarNoSelectionRecordAction component and the corresponding action key in NoSelectionRecordActionKeys. The new action enhances user interaction by allowing sidebar modifications when in edit mode.
2026-03-05 19:32:17 +05:30
Abdul Rahman 1926a07c74 refactor: simplify target ID validation logic in WorkspaceDndKitProvider
Updated the target ID validation logic in the WorkspaceDndKitProvider component to streamline the conditions for determining if the target ID is a string. This change enhances code readability and maintains the existing functionality.
2026-03-05 19:14:42 +05:30
Abdul Rahman 6bc1cf7616 feat: add disableDrag prop to AddToNavigationDragHandle and CommandMenuItemWithAddToNavigationDrag components
Introduced a disableDrag prop to both the AddToNavigationDragHandle and CommandMenuItemWithAddToNavigationDrag components. This enhancement allows for improved control over drag interactions, enabling the disabling of drag functionality based on specific conditions. Updated styles to reflect the new prop, enhancing user experience and interaction clarity in the command menu.
2026-03-05 19:09:49 +05:30
Abdul Rahman 22ac9aeb92 feat: update CommandMenuLinkInfo to use LinkIconWithLinkOverlay for improved link representation
Refactored the CommandMenuLinkInfo component to utilize the new LinkIconWithLinkOverlay component, enhancing the visual representation of links with an optional favicon. Additionally, modified the LinkIconWithLinkOverlay to improve favicon handling by tracking failed favicon links, ensuring better user experience and consistency in the navigation menu.
2026-03-05 19:05:30 +05:30
Abdul Rahman ea9acdaa2e feat: add LinkIconWithLinkOverlay component for enhanced link representation in navigation menu
Introduced the LinkIconWithLinkOverlay component to display a link icon with an optional favicon overlay in the navigation menu. This component utilizes the getLinkFaviconUrl utility to fetch favicons based on provided links and integrates with the NavigationMenuItemIcon to enhance link item rendering. Additionally, updated NavigationMenuItemIcon to utilize the new component for link items, improving visual consistency and user experience.
2026-03-05 17:34:34 +05:30
Abdul Rahman e208bf7c7a feat: integrate isNavigationMenuInEditMode state into action registration logic
Updated the shouldBeRegistered functions across various record actions to incorporate the isNavigationMenuInEditMode state. This enhancement ensures that actions are correctly registered or disabled based on the navigation menu's edit mode, improving user experience and interaction clarity. Additionally, modified related hooks and components to utilize this new state for better control over action availability.
2026-03-05 17:12:04 +05:30
Abdul Rahman 993abf9c8f feat: filter available object metadata items by visibility in command menu
Updated the CommandMenuNewSidebarItemViewPickerSubView component to filter views based on their visibility, ensuring only workspace-visible views are included. Additionally, modified the getAvailableObjectMetadataForNewSidebarItem utility to filter active non-system object metadata items by index view availability, enhancing the accuracy of available items in the command menu.
2026-03-05 16:59:14 +05:30
Abdul Rahman c781b481d6 feat: add iconColor and color properties to command menu item components
Enhanced the CommandMenuObjectMenuItem component by adding an iconColor prop for improved customization. Additionally, updated the buildCreateNavigationMenuItemInput utility to include a color property if defined in the draft item, allowing for better visual representation in navigation menus.
2026-03-05 16:48:51 +05:30
Abdul Rahman f10bfd4499 fix: update ObjectIconWithViewOverlay icon size and stroke for improved visual consistency
Adjusted the size of the ViewIcon from 10px to 12px and changed the stroke from large to medium in the ObjectIconWithViewOverlay component. This update enhances the visual consistency and alignment with the overall design system.
2026-03-05 03:36:57 +05:30
Abdul Rahman b968366d51 feat: introduce disableDrag prop to command menu items for enhanced interaction control
Added a disableDrag prop to various command menu components, including CommandMenuItemWithAddToNavigationDrag and CommandMenuNewSidebarItemMainMenu. This enhancement allows for better control over drag interactions, preventing dragging when certain conditions are met. Updated related components to utilize this new prop, improving user experience and interaction clarity across the command menu.
2026-03-05 03:26:42 +05:30
Abdul Rahman 3dd2a973e6 feat: add disabled state to command menu items for improved user interaction
Enhanced the CommandMenuItemWithAddToNavigationDrag and CommandMenuNewSidebarItemMainMenu components by introducing a disabled state. This allows for better control over user interactions, preventing actions when items are not selectable. Updated styles and logic to reflect the disabled state, improving overall usability and clarity in the command menu.
2026-03-05 03:18:02 +05:30
Abdul Rahman 9e26c7e2b5 refactor: streamline folder content length calculation in WorkspaceNavigationMenuItemsFolder
Refactored the calculation of folder content length in the WorkspaceNavigationMenuItemsFolder component to improve code clarity. The logic for determining the length in edit mode has been simplified, enhancing readability while maintaining functionality.
2026-03-05 02:30:37 +05:30
Abdul Rahman 25b1a5d7a2 refactor: simplify edit mode checks in WorkspaceNavigationMenuItemsFolder
Removed redundant condition for checking edit mode in the WorkspaceNavigationMenuItemsFolder component. This change streamlines the logic for rendering folder content and improves code clarity while maintaining intended functionality.
2026-03-05 02:30:12 +05:30
Abdul Rahman ac442724f1 refactor: improve navigation menu item position handling
Refactored the position handling logic in the useNavigationMenuItemMoveRemove hook to utilize a new getPositionBetween utility for calculating item positions. This change enhances the accuracy of position updates when moving items within the navigation menu. Additionally, updated related functions to directly use draft item positions, improving overall code clarity and functionality.
2026-03-05 01:47:52 +05:30
Abdul Rahman b4836d519e refactor: update click handlers to prevent event propagation in favorites and navigation components
Modified click handlers in various components, including CurrentWorkspaceMemberFavorites, CurrentWorkspaceMemberOrphanFavorites, FavoritesFolderContent, and navigation menu items, to stop event propagation when deleting favorites or navigation items. This change enhances user experience by preventing unintended interactions while maintaining functionality.
2026-03-04 22:32:54 +05:30
Abdul Rahman dcee661a79 refactor: simplify click handler logic in NavigationDrawerItemForObjectMetadataItem
Refactored the click handling logic in the NavigationDrawerItemForObjectMetadataItem component to streamline the conditions for handling clicks based on the edit mode state. This change improves code readability and maintains the intended functionality for navigation interactions.
2026-03-04 22:24:15 +05:30
Abdul Rahman 29e68663e2 feat: add variant prop to navigation components for styling
Enhanced multiple navigation components, including WorkspaceNavigationMenuItemsFolder, WorkspaceSectionAddMenuItemButton, NavigationDrawerItem, and NavigationDrawerSubItem, by introducing a variant prop. This addition allows for improved styling options, specifically supporting a 'tertiary' variant, which enhances visual differentiation and user experience across the navigation interface.
2026-03-04 22:15:57 +05:30
Abdul Rahman eef7175e1e feat: add alwaysShowRightOptions prop to navigation components
Enhanced the WorkspaceNavigationMenuItemsFolder and WorkspaceFolderReadOnly components by adding the alwaysShowRightOptions prop. This improvement ensures that right options are consistently displayed, enhancing user interaction within the navigation menus.
2026-03-04 17:38:16 +05:30
Abdul Rahman 7d0b91c033 feat: add isOpen prop to navigation drawer section titles
Enhanced multiple navigation drawer components by adding the isOpen prop to the NavigationDrawerSectionTitle, allowing for better control of the open/close state. This improves the user experience by providing visual feedback on the navigation sections' status.
2026-03-04 17:32:27 +05:30
Abdul Rahman a3854f6e11 feat: add navigation command for new sidebar item in edit mode
Enhanced the WorkspaceNavigationMenuItems component by integrating a navigation command that triggers when entering edit mode. This allows users to seamlessly navigate to the command menu for adding a new sidebar item, improving the editing experience.
2026-03-04 17:21:55 +05:30
Abdul Rahman 63d452d05f feat: add edit mode check to CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher
Implemented a check for edit mode in the CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher component. If the navigation menu is in edit mode, the component will return null, preventing rendering of the folder items during editing. This enhances user experience by avoiding unnecessary UI elements during editing sessions.
2026-03-04 17:20:11 +05:30
Abdul Rahman bd3343e963 refactor: replace theme access with themeCssVariables in styled components
Updated multiple components to utilize themeCssVariables for styling instead of runtime theme access. This change enhances performance and consistency across the application by leveraging CSS custom properties for spacing, borders, and colors.
2026-03-04 17:18:15 +05:30
Abdul Rahman 02b5b79da1 Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-04 17:07:52 +05:30
Abdul Rahman 6f806a5659 Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-04 02:17:52 +05:30
Abdul Rahman 3723000af0 refactor: simplify styled components in WorkspaceNavigationMenuItemsFolder and WorkspaceFolderReadOnly
Removed unnecessary props from styled components in WorkspaceNavigationMenuItemsFolder and WorkspaceFolderReadOnly, enhancing code clarity and maintainability. This update streamlines the component structure while preserving existing functionality.
2026-03-04 02:14:08 +05:30
Abdul Rahman 5b1149c33a fix: improve destination assignment logic in WorkspaceDndKitProvider for folder drops
Updated the destination assignment logic in the WorkspaceDndKitProvider to handle dropping items into folders more accurately. The logic now checks if the target is a folder and adjusts the droppable ID accordingly, enhancing the drag-and-drop functionality and user experience within the navigation menu.
2026-03-04 02:02:40 +05:30
Abdul Rahman 8719b0ad27 fix: resolve folder ID assignment in useSaveNavigationMenuItemsDraft hook
Updated the folder ID assignment logic in the useSaveNavigationMenuItemsDraft hook to ensure that the folder ID is resolved correctly when it is not null. This change improves the accuracy of the draft item updates while maintaining existing functionality.
2026-03-04 01:54:32 +05:30
Abdul Rahman 86a40c4034 refactor: reorganize imports and enhance component structure in NavigationDrawerSectionForWorkspaceItemsListDndKit
Updated the NavigationDrawerSectionForWorkspaceItemsListDndKit component by reorganizing imports and introducing a new styled component for list item rows. This refactor improves code clarity and maintainability while preserving existing functionality.
2026-03-04 01:51:59 +05:30
Abdul Rahman 6ef92eb86f refactor: streamline WorkspaceNavigationMenuItemsFolder component
Updated the WorkspaceNavigationMenuItemsFolder component by reorganizing imports and removing an unused variable. Enhanced the rendering logic for adding menu items in edit mode, improving code clarity and maintainability. This refactor aims to optimize the component's structure while maintaining existing functionality.
2026-03-04 01:44:40 +05:30
Abdul Rahman ca2986894a feat: enhance drag-and-drop logic in WorkspaceDndKitProvider and related components
Added new functions to improve drag-and-drop interactions, including validation for dropping items into folders and handling effective drop target IDs. Updated WorkspaceDndKitDroppableSlot to support customizable collision priorities, enhancing the drag-and-drop experience. This update aims to streamline user interactions and prevent invalid drop actions within the navigation menu.
2026-03-04 01:38:33 +05:30
Abdul Rahman e09cc62a6b feat: improve drag-and-drop validation in WorkspaceDndKitProvider and related components
Enhanced the drag-and-drop functionality by adding checks to prevent dropping items into folders when the source item is also a folder. Updated the WorkspaceDndKitProvider to utilize the new validation logic and refined the NavigationItemDropTarget and WorkspaceNavigationMenuItemsFolder components to reflect forbidden drop states. This update aims to improve user experience and prevent invalid drop actions within the navigation menu.
2026-03-04 01:18:11 +05:30
Abdul Rahman f6ae0fbfbc feat: enhance drag-and-drop functionality in WorkspaceNavigationMenuItemsFolder
Refactored the WorkspaceNavigationMenuItemsFolder component to improve the rendering of sortable items and integrate NavigationItemDropTarget for better drag-and-drop interactions. This update includes the addition of drop target IDs and enhances user feedback during item manipulation within the workspace navigation menu.
2026-03-04 01:05:32 +05:30
Abdul Rahman b092d118e5 feat: add feedback for drag-and-drop interactions in CommandMenuItem and WorkspaceDndKitSortableItem
Enhanced the drag-and-drop functionality by introducing a 'clone' feedback message in both CommandMenuItemWithAddToNavigationDragDndKit and WorkspaceDndKitSortableItem components. This improvement aims to provide clearer user feedback during drag-and-drop operations.
2026-03-04 01:02:42 +05:30
Abdul Rahman 4bc85e1423 feat: enhance drag-and-drop behavior in WorkspaceDndKitProvider
Introduced a new state for tracking the last drop index and improved the handleDragOver function to calculate drop positions more accurately. Updated the WorkspaceDndKitSortableItem to utilize the SortableKeyboardPlugin, enhancing keyboard accessibility during drag-and-drop operations. Additionally, refactored the WorkspaceNavigationMenuItemsFolder to streamline the rendering of sortable items.
2026-03-04 00:56:11 +05:30
Abdul Rahman 785c987e19 feat: enhance drag-and-drop functionality in FavoritesDragDropProviderContent
Added support for favorites drag-and-drop interactions by introducing the `getFavoritesDropTargetIdFromDestination` utility and integrating it into the `handleDragUpdate` and `handleDragEnd` methods. This enhancement improves user experience when managing favorite items within the navigation menu.
2026-03-03 17:40:31 +05:30
Abdul Rahman 379cbaaef8 refactor: update styled component for NavigationMenuItemDroppable
Refactored the `StyledDroppableWrapper` component to utilize data attributes for drag-and-drop states instead of props. This change enhances code clarity and maintainability while ensuring consistent styling behavior during drag-and-drop interactions.
2026-03-03 17:39:39 +05:30
Abdul Rahman 9726a4415c refactor: remove unused styled component in NavigationDrawerSectionForWorkspaceItems
Eliminated the `StyledWorkspaceDroppableList` styled component and its associated imports to streamline the code and improve maintainability. This change contributes to a cleaner component structure in the NavigationDrawerSectionForWorkspaceItems.
2026-03-03 17:28:11 +05:30
Abdul Rahman 852585709a Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-03 17:27:54 +05:30
Abdul Rahman a4779365a8 feat: wrap CurrentWorkspaceMemberNavigationMenuItemFolders in FavoritesDragDropProviderContent
Enhanced the CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher component by wrapping the CurrentWorkspaceMemberNavigationMenuItemFolders with FavoritesDragDropProviderContent. This change supports drag-and-drop functionality within the navigation menu item editing context, improving user experience during folder management.
2026-03-03 17:26:42 +05:30
Abdul Rahman c800f4158e refactor: simplify styled component for navigation drop target
Refactored the `StyledDropTarget` component by removing unnecessary props and utilizing data attributes for drag-and-drop states. This change enhances code clarity and maintainability while ensuring consistent styling behavior during drag-and-drop interactions.
2026-03-03 17:17:51 +05:30
Abdul Rahman 8023b596a6 refactor: enhance navigation menu item draft saving logic
Refactored the `useSaveNavigationMenuItemsDraft` hook to improve the creation of navigation menu items. Introduced utility functions `buildCreateNavigationMenuItemInput` and `orderFoldersForCreation` to streamline input construction and folder ordering. This change enhances code clarity and maintainability while ensuring correct handling of folder dependencies during item creation.
2026-03-03 08:23:25 +05:30
Abdul Rahman 05be4faaad feat: introduce WorkspaceSectionAddMenuItemButton and WorkspaceSectionListEditModeFallback components
Added new components to enhance the navigation drawer's edit mode functionality. The WorkspaceSectionAddMenuItemButton allows users to add new items directly, while the WorkspaceSectionListEditModeFallback provides a fallback UI for when the navigation menu is in edit mode, integrating read-only item lists and the add button. Updated NavigationDrawerSectionForWorkspaceItems to utilize the new fallback component.
2026-03-03 08:11:40 +05:30
Abdul Rahman 093fd52300 fix: update visibility condition for add menu item button in navigation drawer
Removed the dependency on dragging state for the visibility of the add menu item button in the navigation drawer, ensuring it remains visible when in edit mode.
2026-03-03 07:59:36 +05:30
Abdul Rahman 9e50d70d0c Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-03 07:54:56 +05:30
Abdul Rahman 7bc12ab0ce Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-03 02:57:01 +05:30
Abdul Rahman 2c7780e2c4 Refactor navigation components to enhance drag-and-drop functionality and improve code clarity. This update introduces a new preload mechanism for the drag-and-drop kit, replaces the WorkspaceDndKitPreloadEffect with direct preloading in PageDragDropProvider, and adds read-only components for better rendering in edit mode. Additionally, it consolidates logic in NavigationDrawerSection components and introduces new skeleton loaders for improved user experience during loading states. 2026-03-03 02:54:35 +05:30
Abdul Rahman 8167b68626 Remove WorkspaceDndKitDraggableItem component to streamline drag-and-drop functionality. This deletion enhances code clarity and maintainability by eliminating unused components, contributing to a cleaner codebase. 2026-03-03 01:08:44 +05:30
Abdul Rahman 584e06d0b6 Introduce WorkspaceDndKitPreloadEffect to preload drag-and-drop kit and refactor PageDragDropProvider for improved structure. This change enhances the component's rendering logic by integrating the preload effect, ensuring better performance and maintainability in drag-and-drop functionality. 2026-03-02 15:53:05 +05:30
Abdul Rahman dce9c281ee Refactor WorkspaceDndKitProvider to improve type safety and streamline drag-and-drop event handling. This update introduces new type definitions for drag start, drag over, and drag end events, enhancing clarity and maintainability while ensuring better integration with the drag-and-drop logic. 2026-03-02 15:49:15 +05:30
Abdul Rahman 93640eef04 Add EditModeProps and refactor component imports for improved type management
This commit introduces the EditModeProps type to enhance type safety in edit mode handling. Additionally, it updates component imports in NavigationDrawerSectionForWorkspaceItemContent, NavigationDrawerSectionForWorkspaceItems, and NavigationDrawerSectionForWorkspaceItemsListDndKit to utilize the new type definitions, improving clarity and maintainability across the object metadata module.
2026-03-02 15:36:23 +05:30
Abdul Rahman 46ee272bdd Refactor navigation components to remove unused onAddMenuItem prop and streamline add menu item functionality. This change enhances clarity and maintainability by consolidating logic and improving the drag-and-drop experience in the workspace navigation. 2026-03-02 15:31:33 +05:30
Abdul Rahman 806d72e2b4 Refactor NavigationDrawerSection components to improve clarity and maintainability by removing unused props and consolidating logic. This change enhances the drag-and-drop functionality and streamlines the rendering of workspace items, ensuring a cleaner code structure. 2026-03-02 15:25:13 +05:30
Abdul Rahman 3215a7b9e6 Refactor WorkspaceDndKitProvider to remove FavoritesDragContext and streamline the rendering logic of drag-and-drop components. This change enhances code clarity and maintainability by consolidating context providers and improving the overall structure of the component. 2026-03-02 15:22:13 +05:30
Abdul Rahman 0015085fd5 Refactor WorkspaceDndKitProvider to improve drag-and-drop logic by consolidating condition checks and enhancing readability. This change streamlines the handling of sortable and droppable elements, ensuring clearer flow and better maintainability of the component's drag-and-drop functionality. 2026-03-02 15:13:15 +05:30
Abdul Rahman 8e3f9283b9 Refactor WorkspaceDndKitProvider to enhance drag-and-drop event handling by introducing new type definitions for drag operations. This change improves code clarity and maintainability, streamlining the handling of drag events and ensuring better type safety throughout the component. 2026-03-02 15:10:53 +05:30
Abdul Rahman 9951aa3202 Refactor WorkspaceDndKitProvider to improve drag-and-drop logic by introducing a DropDestination type and consolidating drop result options. This change enhances code clarity and maintainability while streamlining sensor configuration and event handling. 2026-03-02 15:03:41 +05:30
Abdul Rahman 4c4d755daf Refactor PageDragDropProvider and CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher to streamline drag-and-drop logic. Removed unnecessary feature flag checks and consolidated rendering logic, enhancing clarity and maintaining consistent behavior across components. 2026-03-02 14:48:31 +05:30
Abdul Rahman 6ad6cc8e69 Refactor useHandleWorkspaceNavigationMenuItemDragAndDrop to extract getPositionBetween utility function for improved clarity and reusability. This change enhances the drag-and-drop logic by centralizing position calculation, streamlining the component's code. 2026-03-02 14:37:15 +05:30
Abdul Rahman 1b5b2f7fc4 Enhance MainNavigationDrawerScrollableItems by adding Suspense for lazy-loaded components. This change improves loading behavior and user experience by providing a fallback during component loading. 2026-03-02 14:31:48 +05:30
Abdul Rahman f78454b6b0 Refactor navigation components to enhance drag-and-drop functionality by removing the WorkspaceDndKitContext dependency and consolidating the rendering logic. This change improves clarity and maintains consistency in edit mode handling across components, while also deleting the unused NavigationDrawerSectionForWorkspaceItemsListHelloPangea component. 2026-03-02 07:32:17 +05:30
Abdul Rahman 622c89d3a9 Refactor WorkspaceNavigationMenuItemFolderSubItem to improve edit mode handling by introducing isEditableInEditMode variable. This change enhances clarity in the component's logic and maintains consistency with the existing state management approach. 2026-03-02 07:21:15 +05:30
Abdul Rahman 00f2e56925 Refactor navigation menu components to streamline state management by removing objectMetadataItems and views props. Introduced local state retrieval for object metadata and core views, enhancing code clarity and reducing prop drilling across components. 2026-03-02 07:19:12 +05:30
Abdul Rahman cfec6c8e26 Refactor navigation menu components to utilize isNavigationMenuInEditMode state for improved edit mode handling. This change enhances code clarity and consistency across various components by replacing direct isEditMode props with the new state management approach. 2026-03-02 07:15:03 +05:30
Abdul Rahman d6e9e0ca9d Remove useDndKit prop from WorkspaceSectionItemContent and related components to simplify the API and improve clarity in drag-and-drop functionality. 2026-03-02 07:03:33 +05:30
Abdul Rahman 7916f58fe9 Add WorkspaceNavigationMenuItemFolderSubItem component to enhance navigation menu structure. This new component encapsulates the logic for rendering sub-items in the workspace navigation menu, improving code organization and maintainability. 2026-03-02 07:03:14 +05:30
Abdul Rahman b57c6cf7a4 Refactor useHandleWorkspaceNavigationMenuItemDragAndDrop to improve drag-and-drop logic by introducing a new function for calculating item positions. This change enhances the clarity and maintainability of the code while ensuring accurate position updates during item reordering. 2026-03-02 06:56:49 +05:30
Abdul Rahman 1bd326eefe Refactor WorkspaceDndKitDroppableSlot to simplify the component structure by introducing a new variable, isEmpty, for determining the empty state. This change enhances readability and maintains consistency in drag-and-drop functionality. 2026-03-02 06:44:07 +05:30
Abdul Rahman cabb5950af Remove unused isInsideScrollableContainer prop from WorkspaceDndKitDraggableItem component to simplify the code and improve clarity in drag-and-drop functionality. 2026-03-02 06:40:39 +05:30
Abdul Rahman ef4a5ab345 Refactor CommandMenuItemWithAddToNavigationDrag to simplify drag-and-drop logic by removing the dependency on DnD Kit context. Enhanced conditional rendering to improve component structure and user experience when drag index is not defined. 2026-03-02 06:34:20 +05:30
Abdul Rahman b44fe87117 Refactor CommandMenuAddToNavDroppable to simplify drag-and-drop implementation by removing the conditional rendering for DnD Kit. Introduced a fallback provided object to enhance the suspense fallback behavior, improving the overall component structure and user experience. 2026-03-02 06:27:19 +05:30
Abdul Rahman 5e565feef1 Remove CommandMenuAddToNavDraggablePlaceholder component and update references in CommandMenuNewSidebarItemMainMenu and CommandMenuObjectPickerSubView to streamline drag-and-drop functionality. This change simplifies the component structure by eliminating unnecessary placeholders. 2026-03-02 06:19:19 +05:30
Abdul Rahman 4c22c47f6b Refactor CommandMenuAddToNavDroppable and CommandMenuAddToNavDroppableDndKit to use AddToNavDroppableProvided type for improved type safety. Update drop target properties to enhance drag-and-drop functionality consistency across components. Remove unused NavigationDrawerSectionForWorkspaceItemsShared file. 2026-03-02 06:07:51 +05:30
Abdul Rahman 682a7123e1 Refactor WorkspaceSectionListDndKit to utilize WorkspaceOrphanDropTarget for improved structure and readability. Update drop target IDs to use NavigationMenuItemDroppableIds for consistency across components. 2026-03-02 05:52:18 +05:30
Abdul Rahman 20b8153c57 Refactor WorkspaceDndKitProvider to improve readability by introducing a new variable, isAddToNavDrag, for checking drag source. This change enhances the clarity of conditional checks related to drag-and-drop functionality. 2026-03-02 05:48:05 +05:30
Abdul Rahman c2cbfaa73d Refactor CommandMenuSystemObjectPickerSubView to enhance drag-and-drop functionality. Introduced conditional rendering for add variant using CommandMenuAddToNavDroppable, improving user experience when adding system objects to navigation. 2026-03-02 05:41:17 +05:30
Abdul Rahman ab9dfccc35 Enhance drag-and-drop functionality in WorkspaceDndKitProvider and NavigationDrawerSection components by conditionally setting active and forbidden drop targets based on the source droppable ID. Refactor WorkspaceSectionListDndKit to utilize NavigationItemDropTarget for improved structure and readability. 2026-03-02 05:37:36 +05:30
Abdul Rahman 0c14801175 Refactor NavigationDrawerSection components to remove unused theme prop and update prop types for DnD Kit and Hello Pangea sections. 2026-03-01 03:26:59 +05:30
Abdul Rahman 129d429006 Remove unused theme prop from WorkspaceSectionItemContentProps and WorkspaceSectionListCommonProps in NavigationDrawerSectionForWorkspaceItemsTypes. 2026-03-01 03:22:59 +05:30
Abdul Rahman c5d216d69b Refactor NavigationDrawerSectionForWorkspaceItemContent to use getEffectiveNavigationMenuItemColor for icon color and pass folder color directly. Removed unused theme prop. 2026-03-01 03:22:49 +05:30
Abdul Rahman 05a2a1db20 Merge branch 'main' into dnd-kit-navbar-drag-drop 2026-03-01 01:59:45 +05:30
Abdul Rahman e994cf1b8d Remove unused StyledDropIndicator from WorkspaceDndKitSortableItem component
- Eliminated the StyledDropIndicator component, which was previously used to visually indicate drop targets during drag-and-drop operations.
- Updated the WorkspaceDndKitSortableItem to remove references to isDropTarget, streamlining the component's functionality.
2026-02-27 04:06:50 +05:30
Abdul Rahman 89a4b283d7 Implement Drag-and-Drop Enhancements for Workspace Navigation
- Introduced `FavoritesDragDropProviderContent` to manage drag-and-drop interactions for favorite items within the workspace.
- Updated `CurrentWorkspaceMemberFavoritesFolders` to wrap favorite components with the new `FavoritesDragProvider`, enabling drag-and-drop functionality.
- Refactored `PageDragDropProvider` to utilize `FavoritesDragDropProviderContent`, streamlining drag-and-drop logic.
- Added new components for handling workspace navigation items with drag-and-drop capabilities, including `WorkspaceSectionListDndKit` and `WorkspaceSectionListHelloPangea`.
- Enhanced existing components to support the new drag-and-drop context and improve user experience during item manipulation.
2026-02-27 04:04:37 +05:30
Abdul Rahman 5a4f9fbddd Add DnD Kit Drop Target ID Utilities
- Introduced `DndKitDropTargetIdSeparator.ts` to define a constant for separating droppable IDs in drag-and-drop functionality.
- Added `getDndKitDropTargetId.ts` to generate a unique drop target ID using the droppable ID and index.
- Created `parseDropTargetIdToDestination.ts` to parse the drop target ID back into its components, returning the droppable ID and index or null if invalid.
2026-02-27 04:04:11 +05:30
Abdul Rahman ac94ff5e68 Refactor preloadWorkspaceDndKit function for improved readability
- Updated the `preloadWorkspaceDndKit` function to enhance code clarity by adding braces around the return statement when `preloadScheduled` is true. This change improves the readability of the conditional logic.
2026-02-27 03:58:47 +05:30
Abdul Rahman a4a763e787 Enhance Command Menu Drag-and-Drop Functionality
- Updated `CommandMenuAddToNavDraggablePlaceholder.tsx` to conditionally render children based on the `WorkspaceDndKitContext`, allowing for better integration with drag-and-drop features.
- Modified `CommandMenuAddToNavDroppable.tsx` to utilize lazy loading for the `CommandMenuAddToNavDroppableDndKit` component when in DnD mode, improving performance.
- Enhanced `CommandMenuItemWithAddToNavigationDrag.tsx` to support lazy loading of the `CommandMenuItemWithAddToNavigationDragDndKit` component, streamlining drag-and-drop interactions.
- Refactored imports in `CurrentWorkspaceMemberFavorites.tsx` for improved state management and consistency across components.
2026-02-27 03:58:39 +05:30
Abdul Rahman 677305597c Add SortableDropTargetRefContext for Drag-and-Drop Functionality
- Introduced `SortableDropTargetRefContext.tsx`, creating a context for managing references to sortable drop targets in the navigation menu.
- This context will facilitate the integration of drag-and-drop features within the workspace navigation, enhancing user interaction.
2026-02-27 03:56:49 +05:30
Abdul Rahman 8e235cda45 Add WorkspaceDndKitProvider and Context for Drag-and-Drop Functionality
- Introduced `WorkspaceDndKitProvider.tsx`, a new provider component that manages drag-and-drop interactions within the workspace navigation.
- Added `WorkspaceDndKitContext.tsx` to create a context for sharing drag-and-drop state across components.
- Implemented state management for drag events, including handling drag start, drag over, and drop actions, utilizing `@dnd-kit/react` for enhanced drag-and-drop capabilities.
2026-02-27 03:56:05 +05:30
Abdul Rahman 4c664fba7d Add WorkspaceDndKitSortableItem Component
- Introduced `WorkspaceDndKitSortableItem.tsx`, a new component for implementing sortable drag-and-drop functionality within the workspace navigation menu.
- Utilizes `@dnd-kit/react/sortable` for drag-and-drop capabilities and includes styled components for visual indicators during sorting.
2026-02-27 03:53:13 +05:30
Abdul Rahman ca4746a743 Add Command Menu and Workspace DnD Components
- Introduced `CommandMenuItemWithAddToNavigationDragDndKit.tsx` for draggable command menu items with navigation integration.
- Added `WorkspaceDndKitDraggableItem.tsx` to facilitate drag-and-drop functionality for workspace items, supporting custom item components and scrollable containers.
2026-02-27 03:52:28 +05:30
Abdul Rahman 2b028e3b88 Add Command Menu and Workspace DnD Components
- Introduced `CommandMenuItemWithAddToNavigationDragDndKit.tsx` for draggable command menu items with navigation integration.
- Added `WorkspaceDndKitDraggableItem.tsx` to facilitate drag-and-drop functionality for workspace items, supporting custom item components and scrollable containers.
2026-02-27 03:51:43 +05:30
Abdul Rahman 0a333497aa Add preloadWorkspaceDndKit function for optimized workspace drag-and-drop initialization
- Introduced a new file `preloadWorkspaceDndKit.ts` that contains a function to preload the WorkspaceDndKitProvider.
- The function ensures that preloading is scheduled only once, utilizing `requestIdleCallback` or a fallback `setTimeout` for efficient resource loading.
2026-02-27 03:46:32 +05:30
Abdul Rahman 7952793624 Update dependencies for @dnd-kit and @preact/signals-core
- Added @dnd-kit packages (abstract, collision, dom, geometry, react, state) at version 0.3.2 to yarn.lock and package.json.
- Updated @preact/signals-core to version 1.13.0 in yarn.lock.
2026-02-27 03:00:48 +05:30
113 changed files with 3502 additions and 1295 deletions
+1
View File
@@ -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",
@@ -7,6 +7,7 @@ import { RestoreMultipleRecordsAction } from '@/action-menu/actions/record-actio
import { UpdateMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/components/UpdateMultipleRecordsAction';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { CreateNewIndexRecordNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/CreateNewIndexRecordNoSelectionRecordAction';
import { EditNavigationSidebarNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/EditNavigationSidebarNoSelectionRecordAction';
import { CreateNewViewNoSelectionRecord } from '@/action-menu/actions/record-actions/no-selection/components/CreateNewViewNoSelectionRecord';
import { HideDeletedRecordsNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/HideDeletedRecordsNoSelectionRecordAction';
import { ImportRecordsNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/components/ImportRecordsNoSelectionRecordAction';
@@ -116,10 +117,16 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
position: 2,
isPinned: true,
Icon: IconPlus,
shouldBeRegistered: ({ objectPermissions, hasAnySoftDeleteFilterOnView }) =>
(objectPermissions.canUpdateObjectRecords &&
!hasAnySoftDeleteFilterOnView) ??
false,
shouldBeRegistered: ({
objectPermissions,
hasAnySoftDeleteFilterOnView,
isNavigationMenuInEditMode,
}) =>
isNavigationMenuInEditMode
? false
: ((objectPermissions.canUpdateObjectRecords &&
!hasAnySoftDeleteFilterOnView) ??
false),
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
component: <CreateNewIndexRecordNoSelectionRecordAction />,
},
@@ -137,12 +144,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
selectedRecord,
hasAnySoftDeleteFilterOnView,
objectPermissions,
isNavigationMenuInEditMode,
}) =>
(isDefined(selectedRecord) &&
!selectedRecord.isRemote &&
!hasAnySoftDeleteFilterOnView &&
objectPermissions.canSoftDeleteObjectRecords &&
!isDefined(selectedRecord?.deletedAt)) ??
!isDefined(selectedRecord?.deletedAt) &&
!isNavigationMenuInEditMode) ??
false,
availableOn: [
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
@@ -165,13 +174,16 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
isNavigationMenuInEditMode,
}) =>
(objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
!hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false,
isNavigationMenuInEditMode
? false
: ((objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
!hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false),
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
component: <DeleteMultipleRecordsAction />,
},
@@ -191,14 +203,17 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
isRemote,
isShowPage,
hasAnySoftDeleteFilterOnView,
isNavigationMenuInEditMode,
}) =>
(!isRemote &&
isDefined(selectedRecord?.deletedAt) &&
objectPermissions.canSoftDeleteObjectRecords &&
((isDefined(isShowPage) && isShowPage) ||
(isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView))) ??
false,
isNavigationMenuInEditMode
? false
: ((!isRemote &&
isDefined(selectedRecord?.deletedAt) &&
objectPermissions.canSoftDeleteObjectRecords &&
((isDefined(isShowPage) && isShowPage) ||
(isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView))) ??
false),
availableOn: [
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
@@ -220,14 +235,17 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
isNavigationMenuInEditMode,
}) =>
(objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false,
isNavigationMenuInEditMode
? false
: ((objectPermissions.canSoftDeleteObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false),
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
component: <RestoreMultipleRecordsAction />,
},
@@ -241,11 +259,18 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconTrashX,
accent: 'danger',
isPinned: true,
shouldBeRegistered: ({ selectedRecord, objectPermissions, isRemote }) =>
(objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(selectedRecord?.deletedAt)) ??
false,
shouldBeRegistered: ({
selectedRecord,
objectPermissions,
isRemote,
isNavigationMenuInEditMode,
}) =>
isNavigationMenuInEditMode
? false
: ((objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(selectedRecord?.deletedAt)) ??
false),
availableOn: [
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
@@ -267,14 +292,17 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
isRemote,
hasAnySoftDeleteFilterOnView,
numberOfSelectedRecords,
isNavigationMenuInEditMode,
}) =>
(objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false,
isNavigationMenuInEditMode
? false
: ((objectPermissions.canDestroyObjectRecords &&
!isRemote &&
isDefined(hasAnySoftDeleteFilterOnView) &&
hasAnySoftDeleteFilterOnView &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT) ??
false),
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
component: <DestroyMultipleRecordsAction />,
},
@@ -387,8 +415,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconEdit,
accent: 'default',
isPinned: true,
shouldBeRegistered: ({ objectPermissions, isRemote }) =>
objectPermissions.canUpdateObjectRecords && !isRemote,
shouldBeRegistered: ({
objectPermissions,
isRemote,
isNavigationMenuInEditMode,
}) =>
!isNavigationMenuInEditMode &&
objectPermissions.canUpdateObjectRecords &&
!isRemote,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
component: <UpdateMultipleRecordsAction />,
},
@@ -406,7 +440,9 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
objectMetadataItem,
numberOfSelectedRecords,
objectPermissions,
isNavigationMenuInEditMode,
}) =>
!isNavigationMenuInEditMode &&
isDefined(objectMetadataItem?.duplicateCriteria) &&
isDefined(numberOfSelectedRecords) &&
Boolean(objectPermissions.canUpdateObjectRecords) &&
@@ -440,8 +476,10 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconFileImport,
accent: 'default',
isPinned: false,
shouldBeRegistered: ({ hasAnySoftDeleteFilterOnView }) =>
!hasAnySoftDeleteFilterOnView,
shouldBeRegistered: ({
hasAnySoftDeleteFilterOnView,
isNavigationMenuInEditMode,
}) => !isNavigationMenuInEditMode && !hasAnySoftDeleteFilterOnView,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
component: <ImportRecordsNoSelectionRecordAction />,
requiredPermissionFlag: PermissionFlagType.IMPORT_CSV,
@@ -757,6 +795,22 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
),
hotKeys: ['G', 'N'],
},
[NoSelectionRecordActionKeys.EDIT_NAVIGATION_SIDEBAR]: {
type: ActionType.Navigation,
scope: ActionScope.Global,
key: NoSelectionRecordActionKeys.EDIT_NAVIGATION_SIDEBAR,
label: msg`Edit navigation sidebar`,
shortLabel: msg`Edit sidebar`,
position: 30,
Icon: IconLayout,
isPinned: false,
availableOn: [ActionViewType.GLOBAL],
shouldBeRegistered: ({ isFeatureFlagEnabled }) =>
isFeatureFlagEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
),
component: <EditNavigationSidebarNoSelectionRecordAction />,
},
[RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT]: {
key: RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT,
@@ -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 (
<Action
onClick={() => setIsNavigationMenuInEditMode(true)}
closeSidePanelOnCommandMenuListActionExecution
/>
);
};
@@ -13,4 +13,5 @@ export enum NoSelectionRecordActionKeys {
GO_TO_TASKS = 'go-to-tasks',
GO_TO_NOTES = 'go-to-notes',
CREATE_NEW_VIEW = 'create-view',
EDIT_NAVIGATION_SIDEBAR = 'edit-navigation-sidebar',
}
@@ -19,6 +19,7 @@ export type ShouldBeRegisteredFunctionParams = {
isRemote?: boolean;
isNoteOrTask?: boolean;
isSelectAll?: boolean;
isNavigationMenuInEditMode?: boolean;
loadedRecords?: ObjectRecord[];
selectedRecord?: ObjectRecord;
numberOfSelectedRecords?: number;
@@ -9,21 +9,22 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
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,
@@ -134,6 +135,10 @@ export const useShouldActionBeRegisteredParams = ({
[store],
);
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const isFeatureFlagEnabled = (featureFlagKey: FeatureFlagKey) => {
@@ -153,6 +158,7 @@ export const useShouldActionBeRegisteredParams = ({
hasAnySoftDeleteFilterOnView,
isShowPage,
isSelectAll,
isNavigationMenuInEditMode,
selectedRecord,
numberOfSelectedRecords: contextStoreNumberOfSelectedRecords,
viewType: viewType ?? undefined,
@@ -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)}</>;
};
@@ -0,0 +1,7 @@
import type { ReactNode } from 'react';
export type AddToNavDroppableProvided = {
innerRef: (element: HTMLElement | null) => void;
droppableProps: object;
placeholder: ReactNode;
};
@@ -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 <div ref={ref}>{menuItemContent}</div>;
};
@@ -0,0 +1,45 @@
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
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';
type SelectedOption = 'object' | 'record' | 'view' | null;
export const CommandMenuNewSidebarItemPage = () => {
const [selectedOption, setSelectedOption] = useState<SelectedOption>(null);
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const disableDrag = isDefined(addMenuItemInsertionContext);
const handleBackToMain = () => {
setSelectedOption(null);
};
switch (selectedOption) {
case 'view':
return <SidePanelNewSidebarItemViewFlow onBack={handleBackToMain} />;
case 'object':
return <SidePanelNewSidebarItemObjectFlow onBack={handleBackToMain} />;
case 'record':
return (
<SidePanelNewSidebarItemRecordSubView
onBack={handleBackToMain}
disableDrag={disableDrag}
/>
);
default:
return (
<SidePanelNewSidebarItemMainMenu
onSelectRecord={() => setSelectedOption('record')}
onSelectObject={() => setSelectedOption('object')}
onSelectView={() => setSelectedOption('view')}
/>
);
}
};
@@ -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={
<LightIconButton
Icon={IconHeartOff}
onClick={() => deleteFavorite(favorite.id)}
onClick={(e) => {
e.stopPropagation();
deleteFavorite(favorite.id);
}}
accent="tertiary"
/>
}
@@ -1,6 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites';
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
@@ -18,6 +19,7 @@ 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,20 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
accent="tertiary"
/>
}
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<>
<FavoriteFolders isNavigationSectionOpen={isNavigationSectionOpen} />
<AnimatedExpandableContainer
isExpanded={isNavigationSectionOpen}
dimension="height"
mode="fit-content"
containAnimation
>
<FavoritesDragProvider>
<FavoriteFolders />
<CurrentWorkspaceMemberOrphanFavorites />
</>
)}
</FavoritesDragProvider>
</AnimatedExpandableContainer>
</NavigationDrawerSection>
);
};
@@ -63,7 +63,10 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() => deleteFavorite(favorite.id)}
onClick={(e) => {
e.stopPropagation();
deleteFavorite(favorite.id);
}}
accent="tertiary"
/>
}
@@ -54,7 +54,10 @@ export const FavoritesFolderContent = ({
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() => deleteFavorite(favorite.id)}
onClick={(e) => {
e.stopPropagation();
deleteFavorite(favorite.id);
}}
accent="tertiary"
/>
}
@@ -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 && (
@@ -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 = () => {
@@ -11,9 +11,14 @@ 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;
justify-content: center;
@@ -21,9 +26,11 @@ const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
$hasFixedSize ? themeCssVariables.spacing[4] : 'auto'};
width: ${({ $hasFixedSize }) =>
$hasFixedSize ? themeCssVariables.spacing[4] : 'auto'};
opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
&: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 (
<StyledIconSlot
$hasFixedSize={hasBackgroundColor || showCustomContentWithoutWrapper}
$disabled={disabled}
$disableDrag={disableDrag}
>
{isHovered ? (
<IconGripVertical
@@ -3,6 +3,7 @@ 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';
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
@@ -71,16 +72,18 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
accent="tertiary"
/>
}
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<>
<NavigationMenuItemFolders
isNavigationSectionOpen={isNavigationSectionOpen}
/>
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
</>
)}
<AnimatedExpandableContainer
isExpanded={isNavigationSectionOpen}
dimension="height"
mode="fit-content"
containAnimation
>
<NavigationMenuItemFolders />
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
</AnimatedExpandableContainer>
</NavigationDrawerSection>
);
};
@@ -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 <CurrentWorkspaceMemberNavigationMenuItemFolders />;
}
return <CurrentWorkspaceMemberFavoritesFolders />;
return (
<AnimatedEaseInOut isOpen={!isNavigationMenuInEditMode} initial>
{isNavigationMenuItemEditingEnabled ? (
<FavoritesDragDropProviderContent>
<CurrentWorkspaceMemberNavigationMenuItemFolders />
</FavoritesDragDropProviderContent>
) : (
<FavoritesDragDropProviderContent>
<CurrentWorkspaceMemberFavoritesFolders />
</FavoritesDragDropProviderContent>
)}
</AnimatedEaseInOut>
);
};
@@ -291,9 +291,10 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
)
@@ -85,9 +85,10 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
@@ -0,0 +1,117 @@
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 { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/get-navigation-menu-item-icon-style-from-color';
import { getLinkFaviconUrl } from '@/navigation-menu-item/utils/getLinkFaviconUrl';
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;
}>`
position: absolute;
inset: 0;
border-radius: 4px;
box-sizing: border-box;
background-color: ${({ $backgroundColor, $noBackgroundOrBorder }) =>
$noBackgroundOrBorder ? 'transparent' : $backgroundColor};
display: flex;
align-items: center;
justify-content: center;
border: ${({ $borderColor, $noBackgroundOrBorder }) =>
$noBackgroundOrBorder || !$borderColor
? 'none'
: `1px solid ${$borderColor}`};
overflow: hidden;
`;
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 [failedFaviconLink, setFailedFaviconLink] = useState<string | null>(
null,
);
const faviconUrl = getLinkFaviconUrl(link);
const showFavicon =
isDefined(faviconUrl) && failedFaviconLink !== (link ?? null);
const linkStyle = getNavigationMenuItemIconStyleFromColor(
navItemColor ?? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
);
return (
<StyledCompositeContainer>
<StyledMainIconWrapper
$backgroundColor={linkStyle.backgroundColor}
$borderColor={linkStyle.borderColor}
$noBackgroundOrBorder={showFavicon}
>
{showFavicon ? (
<StyledFaviconImage
src={faviconUrl}
alt=""
onError={() => setFailedFaviconLink(link ?? null)}
/>
) : (
<DefaultIcon
size="14px"
stroke={themeCssVariables.icon.stroke.md}
color={linkStyle.iconColor}
/>
)}
</StyledMainIconWrapper>
<StyledLinkOverlay $backgroundColor={themeCssVariables.grayScale.gray4}>
<LinkIcon
size="14px"
stroke={themeCssVariables.icon.stroke.md}
color={themeCssVariables.grayScale.gray10}
/>
</StyledLinkOverlay>
</StyledCompositeContainer>
);
};
@@ -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 (
<StyledDropTarget
$isDragOver={isDragOver}
$isDropForbidden={isDropForbidden}
$compact={compact}
data-drag-over={isDragOver && !isDropForbidden ? 'true' : undefined}
data-drop-forbidden={isDropForbidden ? 'true' : undefined}
>
{children}
</StyledDropTarget>
@@ -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 (
<StyledContainer>
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{t`Layout customization`}
</StyledTitle>
<SaveAndCancelButtons
onSave={handleSave}
onCancel={cancelEditMode}
isSaveDisabled={!isDirty || isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
</StyledContainer>
<AnimatePresence>
{showNavigationMenuEditModeBar && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: theme.animation.duration.normal,
ease: 'easeInOut',
}}
>
<StyledContainer>
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{t`Layout customization`}
</StyledTitle>
<SaveAndCancelButtons
onSave={handleSave}
onCancel={cancelEditMode}
isSaveDisabled={!isDirty || isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
</StyledContainer>
</motion.div>
)}
</AnimatePresence>
);
};
@@ -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 {
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;
}
`;
export const NavigationMenuItemDroppable = ({
@@ -59,9 +47,12 @@ export const NavigationMenuItemDroppable = ({
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
{(provided, snapshot) => (
<StyledDroppableWrapper
isDraggingOver={snapshot.isDraggingOver}
isDragIndicatorVisible={isDragIndicatorVisible}
showDropLine={showDropLine}
data-dragging-over={
snapshot.isDraggingOver && isDragIndicatorVisible
? 'true'
: undefined
}
data-show-drop-line={showDropLine ? 'true' : undefined}
>
<div
ref={provided.innerRef}
@@ -58,9 +58,10 @@ export const NavigationMenuItemFolderContent = ({
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
@@ -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 && (
@@ -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 (
<LinkIconWithLinkOverlay
link={navigationMenuItem.link}
LinkIcon={IconLink}
DefaultIcon={IconWorld}
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
/>
);
}
const iconToUse =
StandardIcon ??
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
@@ -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/get-navigation-menu-item-icon-style-from-color';
@@ -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 = ({
</StyledObjectIconWrapper>
<StyledViewOverlay $backgroundColor={themeCssVariables.grayScale.gray4}>
<ViewIcon
size="10px"
stroke={theme.icon.stroke.lg}
size="12px"
stroke={theme.icon.stroke.md}
color={themeCssVariables.grayScale.gray10}
/>
</StyledViewOverlay>
@@ -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/utils/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 (
<StyledSlotWrapper ref={ref} $empty={isEmpty}>
{children}
</StyledSlotWrapper>
);
};
@@ -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 (
<SortableDropTargetRefContext.Provider value={targetRef}>
<StyledSortableRoot
ref={(el) => {
ref(el);
handleRef?.(el);
}}
>
{children}
</StyledSortableRoot>
</SortableDropTargetRefContext.Provider>
);
};
@@ -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 && onNavigationMenuItemClick
? () =>
onNavigationMenuItemClick({
item: navigationMenuItem,
objectMetadataItem: objectMetadataItem ?? undefined,
})
: undefined;
return (
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.Index
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})
}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
selectedNavigationMenuItemId === navigationMenuItem.id
}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
triggerEvent="CLICK"
/>
);
};
@@ -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 <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
}
@@ -151,7 +149,7 @@ export const WorkspaceNavigationMenuItems = () => {
rightIcon={
isNavigationMenuItemEditingEnabled ? (
<StyledRightIconsContainer>
{isEditMode ? (
{isNavigationMenuInEditMode ? (
<LightIconButton
Icon={IconPlus}
accent="tertiary"
@@ -159,25 +157,21 @@ export const WorkspaceNavigationMenuItems = () => {
onClick={handleAddMenuItem}
/>
) : (
<LightIconButton
Icon={IconTool}
accent="tertiary"
size="small"
onClick={handleEditClick}
/>
<div onMouseEnter={preloadWorkspaceDndKit}>
<LightIconButton
Icon={IconTool}
accent="tertiary"
size="small"
onClick={handleEditClick}
/>
</div>
)}
</StyledRightIconsContainer>
) : undefined
}
onAddMenuItem={
isNavigationMenuItemEditingEnabled && isEditMode
? handleAddMenuItem
: undefined
}
isEditMode={isEditMode}
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
onNavigationMenuItemClick={
isEditMode ? handleNavigationMenuItemClick : undefined
isNavigationMenuInEditMode ? handleNavigationMenuItemClick : undefined
}
onActiveObjectMetadataItemClick={
isNavigationMenuItemEditingEnabled
@@ -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,177 @@ 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();
onEditModeClick?.();
handleToggle();
}
}
};
: 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 (
<StyledFolderContainer $isSelectedInEditMode={isSelectedInEditMode}>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<NavigationMenuItemDroppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`}
isWorkspaceSection={true}
>
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
onClick={handleClick}
className="navigation-drawer-item"
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
isDragging={isDragging}
rightOptions={
isOpen ? (
<IconChevronDown
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronRight
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
)
}
/>
</NavigationMenuItemDroppable>
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 = (
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
onClick={handleClick}
className="navigation-drawer-item"
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
isDragging={isDragging}
alwaysShowRightOptions
rightOptions={
<div
onClick={(e) => {
e.stopPropagation();
handleToggle();
}}
>
{isOpen ? (
<IconChevronDown
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronRight
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
)}
</div>
}
/>
);
return (
<StyledFolderContainer
$isSelectedInEditMode={isSelectedInEditMode}
data-drag-over-header={isDragOverFolderHeader ? 'true' : undefined}
data-forbidden-drop-target={isForbiddenDropTarget ? 'true' : undefined}
>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<div ref={setSortableDropTargetRef ?? undefined}>
<WorkspaceDndKitDroppableSlot
droppableId={folderHeaderDroppableId}
index={0}
disabled={folderContentDropDisabled}
collisionPriority={FOLDER_HEADER_SLOT_COLLISION_PRIORITY}
>
{headerItem}
</WorkspaceDndKitDroppableSlot>
</div>
<StyledFolderExpandableWrapper>
<AnimatedExpandableContainer
isExpanded={isOpen}
dimension="height"
mode="fit-content"
containAnimation
initial={!skipInitialExpandAnimation}
>
<Droppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`}
isDropDisabled={folderContentDropDisabled}
ignoreContainerClipping
renderClone={(provided, snapshot, rubric) => (
<WorkspaceNavigationMenuItemFolderDragClone
draggableProvided={provided}
draggableSnapshot={snapshot}
rubric={rubric}
navigationMenuItems={navigationMenuItems}
navigationMenuItemFolderContentLength={
navigationMenuItemFolderContentLength
}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
<StyledFolderDroppableContent>
{navigationMenuItems.map((navigationMenuItem, index) => (
<React.Fragment key={navigationMenuItem.id}>
<NavigationItemDropTarget
folderId={folderId}
index={index}
sectionId={NavigationSections.WORKSPACE}
compact={isCompact}
dropTargetIdOverride={getDndKitDropTargetId(
folderContentDroppableId,
index,
)}
/>
<WorkspaceDndKitSortableItem
id={navigationMenuItem.id}
index={index}
group={folderContentDroppableId}
disabled={
!isNavigationMenuInEditMode || folderContentDropDisabled
}
>
<WorkspaceNavigationMenuItemFolderSubItem
navigationMenuItem={navigationMenuItem}
index={index}
arrayLength={folderContentLengthForTree}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
onNavigationMenuItemClick={onNavigationMenuItemClick}
selectedNavigationMenuItemId={
selectedNavigationMenuItemId ?? null
}
isContextDragging={isContextDragging}
/>
</WorkspaceDndKitSortableItem>
</React.Fragment>
))}
<WorkspaceDndKitDroppableSlot
droppableId={folderContentDroppableId}
index={navigationMenuItems.length}
disabled={folderContentDropDisabled}
>
<NavigationItemDropTarget
folderId={folderId}
index={navigationMenuItems.length}
sectionId={NavigationSections.WORKSPACE}
compact
dropTargetIdOverride={getDndKitDropTargetId(
folderContentDroppableId,
navigationMenuItems.length,
)}
/>
)}
getContainerForClone={() => document.body}
>
{(provided) => (
<StyledFolderDroppableContent
ref={provided.innerRef}
$compact={isEditMode || navigationMenuItems.length === 0}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => {
const objectMetadataItem =
navigationMenuItem.itemType ===
NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType ===
NavigationMenuItemType.RECORD
? getObjectMetadataForNavigationMenuItem(
navigationMenuItem,
objectMetadataItems,
views,
)
: null;
const handleEditModeClick =
isEditMode &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.itemType ===
NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem))
? () =>
onNavigationMenuItemClick({
item: navigationMenuItem,
objectMetadataItem:
objectMetadataItem ?? undefined,
})
: undefined;
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.Index
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})
}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
selectedNavigationMenuItemId ===
navigationMenuItem.id
}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
triggerEvent="CLICK"
/>
}
/>
);
})}
{provided.placeholder}
</StyledFolderDroppableContent>
)}
</Droppable>
{isEditMode && isSelectedInEditMode && (
<NavigationDrawerSubItem
label={t`Add menu item`}
Icon={IconPlus}
onClick={handleAddMenuItemToFolder}
triggerEvent="CLICK"
subItemState={getNavigationSubItemLeftAdornment({
index: navigationMenuItems.length,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
/>
)}
{isNavigationMenuInEditMode && (
<NavigationDrawerSubItem
label={t`Add menu item`}
Icon={IconPlus}
onClick={handleAddMenuItemToFolder}
triggerEvent="CLICK"
variant="tertiary"
isSelectedInEditMode={
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
addMenuItemInsertionContext?.targetFolderId === folderId
}
subItemState={getNavigationSubItemLeftAdornment({
index: navigationMenuItems.length,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
/>
)}
</WorkspaceDndKitDroppableSlot>
</StyledFolderDroppableContent>
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
@@ -0,0 +1 @@
export const DND_KIT_DROP_TARGET_ID_SEPARATOR = '::';
@@ -0,0 +1,5 @@
import { createContext } from 'react';
export const SortableDropTargetRefContext = createContext<
((element: Element | null) => void) | null
>(null);
@@ -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<typeof openNavigationMenuItemInSidePanel>[0],
options: Omit<
Parameters<typeof openNavigationMenuItemInSidePanel>[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,
],
@@ -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);
};
@@ -4,21 +4,7 @@ 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 { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
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 { getPositionBetween } from '@/navigation-menu-item/utils/getPositionBetween';
export const useNavigationMenuItemMoveRemove = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
@@ -44,8 +30,15 @@ export const useNavigationMenuItemMoveRemove = () => {
);
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,
);
});
};
@@ -70,8 +63,15 @@ 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,
);
});
};
@@ -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,
@@ -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,
@@ -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<string, string>();
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;
@@ -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,
};
};
@@ -1,4 +1,5 @@
export type AddMenuItemInsertionContext = {
targetFolderId: string | null;
targetIndex: number;
disableDrag?: boolean;
};
@@ -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;
};
@@ -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;
}
};
@@ -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}`;
@@ -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;
};
@@ -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;
}
};
@@ -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;
};
@@ -0,0 +1,24 @@
export const orderFoldersForCreation = <
T extends { id: string; folderId?: string | null },
>(
folders: T[],
existingIds: Set<string>,
): 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;
};
@@ -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<string | null>(
null,
);
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
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<OnDragUpdateResponder>[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 (
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
<FavoritesDragContext.Provider value={{ isDragging }}>
<NavigationDropTargetContext.Provider
value={{
activeDropTargetId,
setActiveDropTargetId,
forbiddenDropTargetId,
setForbiddenDropTargetId,
addToNavigationFallbackDestination,
}}
>
<DragDropContext
onDragStart={handleDragStart}
onDragUpdate={handleDragUpdate}
onDragEnd={handleDragEnd}
>
{children}
</DragDropContext>
</NavigationDropTargetContext.Provider>
</FavoritesDragContext.Provider>
</NavigationMenuItemDragContext.Provider>
</NavigationDragSourceContext.Provider>
);
};
@@ -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 (
<StyledScrollableItemsContainer>
<NavigationDrawerOpenedSection />
<Suspense fallback={null}>
<Suspense fallback={<NavigationDrawerWorkspaceSectionSkeletonLoader />}>
<CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher />
</Suspense>
<Suspense fallback={null}>
<WorkspaceNavigationMenuItemsDispatcher />
</Suspense>
<RemoteNavigationDrawerSection />
@@ -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,28 @@ export const NavigationDrawerOtherSection = () => {
<NavigationDrawerSectionTitle
label={t`Other`}
onClick={toggleNavigationSection}
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<>
<NavigationDrawerItem
label={t`Settings`}
Icon={IconSettings}
onClick={handleSettingsClick}
/>
<NavigationDrawerItem
label={t`Documentation`}
to={getDocumentationUrl({
locale: currentWorkspaceMember?.locale,
})}
Icon={IconHelpCircle}
/>
</>
)}
<AnimatedExpandableContainer
isExpanded={isNavigationSectionOpen}
dimension="height"
mode="fit-content"
containAnimation
>
<NavigationDrawerItem
label={t`Settings`}
Icon={IconSettings}
onClick={handleSettingsClick}
/>
<NavigationDrawerItem
label={t`Documentation`}
to={getDocumentationUrl({
locale: currentWorkspaceMember?.locale,
})}
Icon={IconHelpCircle}
/>
</AnimatedExpandableContainer>
</NavigationDrawerSection>
);
};
@@ -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<string | null>(
null,
);
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
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<OnDragUpdateResponder>[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 (
<>
<PageDragDropProviderMountEffect
onEnterEditMode={() => setHasProviderMounted(true)}
/>
{children}
</>
);
}
return (
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
<FavoritesDragContext.Provider value={{ isDragging }}>
<NavigationDropTargetContext.Provider
value={{
activeDropTargetId,
setActiveDropTargetId,
forbiddenDropTargetId,
setForbiddenDropTargetId,
addToNavigationFallbackDestination,
}}
>
<DragDropContext
onDragStart={handleDragStart}
onDragUpdate={handleDragUpdate}
onDragEnd={handleDragEnd}
>
{children}
</DragDropContext>
</NavigationDropTargetContext.Provider>
</FavoritesDragContext.Provider>
</NavigationMenuItemDragContext.Provider>
</NavigationDragSourceContext.Provider>
<Suspense fallback={<>{children}</>}>
<LazyWorkspaceDndKitProvider>{children}</LazyWorkspaceDndKitProvider>
</Suspense>
);
};
@@ -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;
};
@@ -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/utils/workspaceDndKitDraggableData';
import { useWorkspaceDndKit } from '@/navigation/hooks/useWorkspaceDndKit';
const WORKSPACE_DND_SENSORS = [
PointerSensor.configure({
activationConstraints: [
new PointerActivationConstraints.Distance({ value: 8 }),
],
}),
KeyboardSensor,
];
type WorkspaceDndKitProviderProps = {
children: ReactNode;
};
export const WorkspaceDndKitProvider = ({
children,
}: WorkspaceDndKitProviderProps) => {
const { contextValues, handlers } = useWorkspaceDndKit();
return (
<NavigationDragSourceContext.Provider value={contextValues.dragSource}>
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
<DragDropProvider<DraggableData>
sensors={WORKSPACE_DND_SENSORS}
onDragStart={handlers.onDragStart}
onDragOver={handlers.onDragOver}
onDragEnd={handlers.onDragEnd}
>
{children}
</DragDropProvider>
</NavigationDropTargetContext.Provider>
</NavigationMenuItemDragContext.Provider>
</NavigationDragSourceContext.Provider>
);
};
@@ -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 type { DraggableData } from '@/navigation/utils/workspaceDndKitDraggableData';
import type { DropDestination } from '@/navigation/utils/workspaceDndKitDropDestination';
import { DROP_RESULT_OPTIONS } from '@/navigation/utils/workspaceDndKitDropResultOptions';
import { isFolderDrag } from '@/navigation/utils/workspaceDndKitIsFolderDrag';
import { resolveDropTarget } from '@/navigation/utils/workspaceDndKitResolveDropTarget';
import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult';
type DragStartPayload = Parameters<
NonNullable<
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragStart']
>
>[0];
type DragOverPayload = Parameters<
NonNullable<
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
>
>[0];
type DragEndPayload = Parameters<
NonNullable<
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragEnd']
>
>[0];
export type WorkspaceDndKitContextValues = {
dragSource: { sourceDroppableId: string | null };
drag: { isDragging: boolean };
dropTarget: {
activeDropTargetId: string | null;
setActiveDropTargetId: (id: string | null) => void;
forbiddenDropTargetId: string | null;
setForbiddenDropTargetId: (id: string | null) => void;
addToNavigationFallbackDestination: DropDestination | null;
};
};
export const useWorkspaceDndKit = (): {
contextValues: WorkspaceDndKitContextValues;
handlers: {
onDragStart: (event: DragStartPayload) => void;
onDragOver: (event: DragOverPayload) => void;
onDragEnd: (event: DragEndPayload) => void;
};
} => {
const store = useStore();
const [isDragging, setIsDragging] = useState(false);
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
null,
);
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
null,
);
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
string | null
>(null);
const [
addToNavigationFallbackDestination,
setAddToNavigationFallbackDestination,
] = useState<DropDestination | null>(null);
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
useHandleWorkspaceNavigationMenuItemDragAndDrop();
const orphanItemCount = workspaceNavigationMenuItems.filter(
(item: { folderId?: string | null }) => !isDefined(item.folderId),
).length;
const getNavItemById = useCallback(
(id: string | undefined) =>
id
? workspaceNavigationMenuItems.find((item) => item.id === id)
: undefined,
[workspaceNavigationMenuItems],
);
const applyWorkspaceReorderIfAllowed = (
id: string,
source: DropDestination,
destination: DropDestination,
) => {
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,
},
};
};
@@ -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();
};
@@ -0,0 +1,4 @@
export type DraggableData = {
sourceDroppableId?: string;
sourceIndex?: number;
};
@@ -0,0 +1 @@
export type DropDestination = { droppableId: string; index: number };
@@ -0,0 +1,6 @@
export const DROP_RESULT_OPTIONS = {
reason: 'DROP' as const,
combine: null,
mode: 'FLUID' as const,
type: 'DEFAULT' as const,
};
@@ -0,0 +1,4 @@
export type DroppableData = {
droppableId: string;
index: number;
};
@@ -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/utils/workspaceDndKitDropDestination';
import type { SortableTargetDestination } from '@/navigation/utils/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,
};
};
@@ -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));
@@ -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/utils/workspaceDndKitDroppableData';
import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget';
import type { ResolvedDropTarget } from '@/navigation/utils/workspaceDndKitResolvedDropTarget';
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,
): ResolvedDropTarget | 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;
};
@@ -0,0 +1,4 @@
import type { SortableTargetDestination } from '@/navigation/utils/workspaceDndKitSortableTargetDestination';
/** Shared result shape for both sortable and string-id targets. Used by drag-over and drag-end. */
export type ResolvedDropTarget = SortableTargetDestination;
@@ -0,0 +1,8 @@
import type { DropDestination } from '@/navigation/utils/workspaceDndKitDropDestination';
export type SortableTargetDestination = {
destination: DropDestination;
effectiveDropTargetId: string;
isTargetFolder: boolean;
dropTargetId: string;
};
@@ -0,0 +1,20 @@
import type { DraggableData } from '@/navigation/utils/workspaceDndKitDraggableData';
import type { DropDestination } from '@/navigation/utils/workspaceDndKitDropDestination';
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,
};
};
@@ -0,0 +1,4 @@
export type EditModeProps = {
isSelectedInEditMode: boolean;
onEditModeClick?: () => void;
};
@@ -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}
/>
);
};
@@ -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 <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
return null;
}
return (
@@ -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}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen &&
@@ -127,7 +126,6 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
<NavigationDrawerItemForObjectMetadataItem
key={`navigation-drawer-item-${objectMetadataItem.id}`}
objectMetadataItem={objectMetadataItem}
isEditMode={isEditMode}
isSelectedInEditMode={
selectedObjectMetadataItemId === objectMetadataItem.id
}
@@ -0,0 +1,64 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
type NavigationDrawerSectionForWorkspaceItemContentProps =
WorkspaceSectionItemContentProps;
export const NavigationDrawerSectionForWorkspaceItemContent = ({
item,
editModeProps,
isDragging,
folderChildrenById,
folderCount,
selectedNavigationMenuItemId,
onNavigationMenuItemClick,
onActiveObjectMetadataItemClick,
readOnly,
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
switch (item.itemType) {
case NavigationMenuItemType.FOLDER:
return (
<NavigationDrawerSectionForWorkspaceItemFolderContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
readOnly={readOnly}
/>
);
case NavigationMenuItemType.LINK:
return (
<NavigationDrawerSectionForWorkspaceItemLinkContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly={readOnly}
/>
);
default:
return (
<NavigationDrawerSectionForWorkspaceItemObjectContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly={readOnly}
/>
);
}
};
@@ -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 (
<WorkspaceFolderReadOnly
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
/>
);
}
return (
<Suspense
fallback={
<WorkspaceFolderReadOnly
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
/>
}
>
<LazyWorkspaceNavigationMenuItemsFolder
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
onNavigationMenuItemClick={onNavigationMenuItemClick}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
isDragging={isDragging}
/>
</Suspense>
);
};
@@ -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 (
<NavigationDrawerItem
label={linkItem.labelIdentifier}
to={isNavigationMenuInEditMode || isDragging ? undefined : linkItem.link}
onClick={
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
active={false}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
isDragging={isDragging}
triggerEvent="CLICK"
rightOptions={
!isNavigationMenuInEditMode && (
<IconArrowUpRight
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.md}
color={themeCssVariables.font.color.light}
/>
)
}
/>
);
};
@@ -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 (
<NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem}
navigationMenuItem={item as ProcessedNavigationMenuItem}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
isDragging={isDragging}
onActiveItemClickWhenNotInEditMode={
onActiveObjectMetadataItemClick
? () => onActiveObjectMetadataItemClick(objectMetadataItem, item.id)
: undefined
}
/>
);
};
@@ -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,47 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
label={sectionTitle}
onClick={() => toggleNavigationSection()}
rightIcon={rightIcon}
alwaysShowRightIcon={isEditMode}
alwaysShowRightIcon={isNavigationMenuInEditMode}
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
{(isNavigationSectionOpen || isAddToNavigationDropTargetVisible) && (
<Droppable
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
isDropDisabled={workspaceDropDisabled}
>
{(provided) => (
<StyledWorkspaceDroppableList
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{filteredItems.map((item, index) => {
const type = item.itemType;
const editModeProps = getEditModeProps(item);
if (type === 'folder') {
return (
<NavigationItemDropTarget
key={item.id}
folderId={null}
index={index}
sectionId={NavigationSections.WORKSPACE}
>
<DraggableItem
draggableId={item.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<WorkspaceNavigationMenuItemsFolder
folderId={item.id}
folderName={item.name ?? 'Folder'}
folderIconKey={item.Icon}
folderColor={
'color' in item ? item.color : undefined
}
navigationMenuItems={
folderChildrenById.get(item.id) ?? []
}
isGroup={folderCount > 1}
isEditMode={isEditMode}
isSelectedInEditMode={
editModeProps.isSelectedInEditMode
}
onEditModeClick={editModeProps.onEditModeClick}
onNavigationMenuItemClick={
onNavigationMenuItemClick
}
selectedNavigationMenuItemId={
selectedNavigationMenuItemId
}
isDragging={isDragging}
/>
}
/>
</NavigationItemDropTarget>
);
<AnimatedExpandableContainer
isExpanded={
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
}
dimension="height"
mode="fit-content"
containAnimation
>
{isNavigationMenuInEditMode ? (
<Suspense
fallback={
<WorkspaceSectionListEditModeFallback
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={
onActiveObjectMetadataItemClick
}
if (type === 'link') {
const linkItem = item as ProcessedNavigationMenuItem;
return (
<NavigationItemDropTarget
key={item.id}
folderId={null}
index={index}
sectionId={NavigationSections.WORKSPACE}
>
<DraggableItem
draggableId={item.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<NavigationDrawerItem
label={linkItem.labelIdentifier}
to={
isEditMode || isDragging
? undefined
: linkItem.link
}
onClick={
isEditMode
? editModeProps.onEditModeClick
: undefined
}
Icon={IconLink}
iconColor={linkItem.color}
active={false}
isSelectedInEditMode={
editModeProps.isSelectedInEditMode
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
}
/>
</NavigationItemDropTarget>
);
}
const objectMetadataItem =
getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
objectMetadataItems,
views,
);
if (!objectMetadataItem) return null;
return (
<NavigationItemDropTarget
key={item.id}
folderId={null}
index={index}
sectionId={NavigationSections.WORKSPACE}
>
<DraggableItem
draggableId={item.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem}
navigationMenuItem={
item as ProcessedNavigationMenuItem
}
isEditMode={isEditMode}
isSelectedInEditMode={
editModeProps.isSelectedInEditMode
}
onEditModeClick={editModeProps.onEditModeClick}
isDragging={isDragging}
onActiveItemClickWhenNotInEditMode={
onActiveObjectMetadataItemClick
? () =>
onActiveObjectMetadataItemClick(
objectMetadataItem,
item.id,
)
: undefined
}
/>
}
/>
</NavigationItemDropTarget>
);
})}
<NavigationItemDropTarget
folderId={null}
index={filteredItems.length}
sectionId={NavigationSections.WORKSPACE}
compact={!isAddMenuItemButtonVisible}
>
{isAddMenuItemButtonVisible && (
<NavigationDrawerItem
Icon={IconPlus}
label={t`Add menu item`}
onClick={onAddMenuItem}
triggerEvent="CLICK"
/>
)}
</NavigationItemDropTarget>
{addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
addToNavigationFallbackDestination.index >
filteredItems.length && (
<NavigationItemDropTarget
folderId={null}
index={addToNavigationFallbackDestination.index}
sectionId={NavigationSections.WORKSPACE}
compact
/>
)}
{provided.placeholder}
</StyledWorkspaceDroppableList>
)}
</Droppable>
)}
/>
}
>
<LazyWorkspaceSectionListDndKit
filteredItems={filteredItems}
getEditModeProps={getEditModeProps}
folderChildrenById={folderChildrenById}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
</Suspense>
) : (
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
)}
</AnimatedExpandableContainer>
</NavigationDrawerSection>
);
};
@@ -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 (
<StyledList>
{filteredItems.map((item, index) => (
<StyledListItemRow key={item.id}>
<WorkspaceOrphanDropTarget index={index} compact />
<WorkspaceDndKitSortableItem
id={item.id}
index={index}
group={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
disabled={!isNavigationMenuInEditMode || workspaceDropDisabled}
>
<NavigationDrawerSectionForWorkspaceItemContent
item={item}
editModeProps={getEditModeProps(item)}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
</WorkspaceDndKitSortableItem>
</StyledListItemRow>
))}
<WorkspaceDndKitDroppableSlot
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
index={filteredItems.length}
disabled={workspaceDropDisabled}
>
<WorkspaceOrphanDropTarget
index={filteredItems.length}
compact={!isAddMenuItemButtonVisible}
>
{isAddMenuItemButtonVisible && <WorkspaceSectionAddMenuItemButton />}
</WorkspaceOrphanDropTarget>
</WorkspaceDndKitDroppableSlot>
{addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
addToNavigationFallbackDestination.index > filteredItems.length && (
<WorkspaceDndKitDroppableSlot
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
index={addToNavigationFallbackDestination.index}
disabled={workspaceDropDisabled}
>
<WorkspaceOrphanDropTarget
index={addToNavigationFallbackDestination.index}
compact
/>
</WorkspaceDndKitDroppableSlot>
)}
</StyledList>
);
};
@@ -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 (
<StyledList>
{filteredItems.map((item: FlatWorkspaceItem) => (
<NavigationDrawerSectionForWorkspaceItemContent
key={item.id}
item={item}
editModeProps={READ_ONLY_EDIT_MODE_PROPS}
isDragging={false}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={null}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly
/>
))}
</StyledList>
);
};
@@ -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`
padding-left: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing['0.5']};
height: ${themeCssVariables.spacing[5]};
display: flex;
align-items: center;
`;
const StyledRowsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[1]};
padding-left: ${themeCssVariables.spacing[1]};
`;
export const NavigationDrawerWorkspaceSectionSkeletonLoader = () => {
return (
<NavigationDrawerSection>
<SkeletonTheme
baseColor={themeCssVariables.background.tertiary}
highlightColor={themeCssVariables.background.transparent.light}
borderRadius={4}
>
<StyledTitleSkeleton>
<Skeleton
width={72}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs}
/>
</StyledTitleSkeleton>
<StyledRowsContainer>
<Skeleton
width={196}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width={196}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width={196}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</StyledRowsContainer>
</SkeletonTheme>
</NavigationDrawerSection>
);
};
@@ -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 (
<StyledFolderContainer>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
onClick={handleToggle}
className="navigation-drawer-item"
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
alwaysShowRightOptions
rightOptions={
isOpen ? (
<IconChevronDown
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.sm}
color={themeCssVariables.font.color.tertiary}
/>
) : (
<IconChevronRight
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.sm}
color={themeCssVariables.font.color.tertiary}
/>
)
}
/>
<StyledFolderExpandableWrapper>
<AnimatedExpandableContainer
isExpanded={isOpen}
dimension="height"
mode="fit-content"
containAnimation
initial={!skipInitialExpandAnimation}
>
<StyledFolderContent>
{navigationMenuItems.map((navigationMenuItem, index) => (
<WorkspaceNavigationMenuItemFolderSubItem
key={navigationMenuItem.id}
navigationMenuItem={navigationMenuItem}
index={index}
arrayLength={navigationMenuItems.length}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
onNavigationMenuItemClick={undefined}
selectedNavigationMenuItemId={null}
isContextDragging={false}
/>
))}
</StyledFolderContent>
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
</StyledFolderContainer>
);
};
@@ -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) => (
<NavigationItemDropTarget
folderId={null}
index={index}
sectionId={NavigationSections.WORKSPACE}
compact={compact}
dropTargetIdOverride={getDndKitDropTargetId(
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
index,
)}
>
{children}
</NavigationItemDropTarget>
);
@@ -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 (
<NavigationDrawerItem
Icon={IconPlus}
label={t`Add menu item`}
onClick={handleClick}
triggerEvent="CLICK"
variant="tertiary"
isSelectedInEditMode={isSelected}
/>
);
};
@@ -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<string, ProcessedNavigationMenuItem[]>;
folderCount: number;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => void;
readOnly?: boolean;
};
@@ -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<string, ProcessedNavigationMenuItem[]>;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => void;
};
@@ -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) => (
<StyledFallback>
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
<WorkspaceSectionAddMenuItemButton />
</StyledFallback>
);
@@ -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,
})
);
};
@@ -1,9 +1,11 @@
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';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
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';
@@ -49,10 +51,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,
@@ -1,8 +1,10 @@
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 { 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';
@@ -19,6 +21,9 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
useRecordTableContextOrThrow();
const isMobile = useIsMobile();
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
@@ -30,10 +35,12 @@ export const RecordTableHeaderLabelIdentifierCellPlusButton = () => {
});
};
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const isReadOnly =
isNavigationMenuInEditMode ||
isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue(
hasAnySoftDeleteFilterOnViewComponentSelector,
@@ -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 (
<Droppable
droppableId={ADD_TO_NAV_SOURCE_DROPPABLE_ID}
isDropDisabled={isDropDisabled}
>
{(provided) => children(provided)}
</Droppable>
<Suspense fallback={children(FALLBACK_PROVIDED)}>
<CommandMenuAddToNavDroppableDndKit
children={children}
isDropDisabled={isDropDisabled}
/>
</Suspense>
);
};
@@ -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'};
width: 100%;
pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
&: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 = (
<StyledDraggableMenuItem
$disabled={disabled}
$disableDrag={disableDrag}
onMouseEnter={() => {
setIsHovered(true);
registerPayload();
if (!disabled && !disableDrag) {
setIsHovered(true);
registerPayload();
}
}}
onMouseLeave={() => {
if (!disabled && !disableDrag) setIsHovered(false);
}}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={registerPayload}
>
<CommandMenuItem
@@ -80,27 +107,22 @@ export const SidePanelItemWithAddToNavigationDrag = ({
description={contextualDescription}
id={id}
onClick={onClick}
disabled={disabled}
/>
</StyledDraggableMenuItem>
);
if (dragIndex !== undefined) {
return (
<Draggable draggableId={id} index={dragIndex} isDragDisabled={false}>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.dragHandleProps}
>
{menuItemContent}
</div>
)}
</Draggable>
);
if (!isDefined(dragIndex) || disableDrag) {
return menuItemContent;
}
return menuItemContent;
return (
<Suspense fallback={menuItemContent}>
<CommandMenuItemWithAddToNavigationDragDndKit
id={id}
dragIndex={dragIndex}
menuItemContent={menuItemContent}
/>
</Suspense>
);
};
@@ -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 (
<SidePanelPageInfoLayout
icon={
<NavigationMenuItemStyleIcon
Icon={IconLink}
<LinkIconWithLinkOverlay
link={selectedItem.link}
LinkIcon={IconLink}
DefaultIcon={IconWorld}
color={selectedItem.color}
/>
}
@@ -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 =
rawPageComponent && React.isValidElement(rawPageComponent)
? React.cloneElement(rawPageComponent, {
key: sidePanelPageInfo.instanceId,
})
: rawPageComponent;
const { theme } = useContext(ThemeContext);
@@ -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<string | null>(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<HTMLInputElement>) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, { link: getAbsoluteUrl(value) });
setUrlEditInput('');
}
};
return (
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
<SidePanelGroup heading={t`Customize`}>
<SidePanelEditColorOption
navigationMenuItemId={selectedItem.id}
color={parseThemeColor(selectedItem.color)}
/>
<TextInput
fullWidth
placeholder="www.google.com"
value={urlEditInput || selectedItem.link}
onChange={(value) => setUrlEditInput(value)}
onBlur={(event) => {
const value = event.target.value.trim();
if (isNonEmptyString(value)) {
onUpdateLink(selectedItem.id, getAbsoluteUrl(value));
setUrlEditInput('');
}
}}
onChange={handleUrlChange}
onBlur={handleUrlBlur}
/>
</SidePanelGroup>
<SidePanelEditOrganizeActions
@@ -1,14 +1,4 @@
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
import { SidePanelEditFolderPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditFolderPickerSubView';
import { SidePanelEditLinkItemView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView';
import { SidePanelEditObjectViewBase } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditObjectViewBase';
import { SidePanelEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
import { useNavigationMenuItemEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
@@ -18,6 +8,16 @@ import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigatio
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
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 { SidePanelEditFolderPickerSubView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditFolderPickerSubView';
import { SidePanelEditLinkItemView } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditLinkItemView';
import { SidePanelEditObjectViewBase } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditObjectViewBase';
import { SidePanelEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
import { SidePanelEditOwnerSection } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOwnerSection';
import { useNavigationMenuItemEditOrganizeActions } from '@/side-panel/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewKey } from '@/views/types/ViewKey';
@@ -132,7 +132,9 @@ export const SidePanelNavigationMenuItemEditPage = () => {
<SidePanelEditLinkItemView
key={selectedItem.id}
selectedItem={selectedItem}
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
onUpdateLink={(linkId, updates) =>
updateLinkInDraft(linkId, updates)
}
onOpenFolderPicker={openFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
@@ -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 (
<SidePanelAddToNavigationDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<SidePanelList
commandGroups={[]}
selectableItemIds={['object', 'view', 'record', 'folder', 'link']}
>
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<SidePanelGroup heading={t`Data`}>
<SidePanelAddToNavigationDraggablePlaceholder index={0}>
<SelectableListItem itemId="object" onEnter={onSelectObject}>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconBuildingSkyscraper}
color="blue"
/>
)}
label={t`Object`}
id="object"
hasSubMenu={true}
onClick={onSelectObject}
/>
</SelectableListItem>
</SidePanelAddToNavigationDraggablePlaceholder>
<SidePanelAddToNavigationDraggablePlaceholder index={1}>
<SelectableListItem itemId="view" onEnter={onSelectView}>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconTable}
color="gray"
/>
)}
label={t`View`}
id="view"
hasSubMenu={true}
onClick={onSelectView}
/>
</SelectableListItem>
</SidePanelAddToNavigationDraggablePlaceholder>
<SidePanelAddToNavigationDraggablePlaceholder index={2}>
<SelectableListItem itemId="record" onEnter={onSelectRecord}>
<CommandMenuItem
Icon={() => (
<Avatar
placeholder="L"
type="rounded"
backgroundColor={theme.color.green4}
/>
)}
label={t`Record`}
id="record"
hasSubMenu={true}
onClick={onSelectRecord}
/>
</SelectableListItem>
</SidePanelAddToNavigationDraggablePlaceholder>
<SelectableListItem
itemId={NavigationMenuItemType.OBJECT}
onEnter={onSelectObject}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconBuildingSkyscraper}
color="blue"
/>
)}
label={t`Object`}
id={NavigationMenuItemType.OBJECT}
hasSubMenu={true}
onClick={onSelectObject}
/>
</SelectableListItem>
<SelectableListItem
itemId={NavigationMenuItemType.VIEW}
onEnter={onSelectView}
>
<CommandMenuItem
Icon={() => (
<NavigationMenuItemStyleIcon
Icon={IconTable}
color="gray"
/>
)}
label={t`View`}
id={NavigationMenuItemType.VIEW}
hasSubMenu={true}
onClick={onSelectView}
/>
</SelectableListItem>
<SelectableListItem
itemId={NavigationMenuItemType.RECORD}
onEnter={onSelectRecord}
>
<CommandMenuItem
Icon={() => (
<Avatar
placeholder="L"
type="rounded"
backgroundColor={themeCssVariables.color.green4}
/>
)}
label={t`Record`}
id={NavigationMenuItemType.RECORD}
hasSubMenu={true}
onClick={onSelectRecord}
/>
</SelectableListItem>
</SidePanelGroup>
<SidePanelGroup heading={t`Other`}>
<SelectableListItem itemId="folder" onEnter={handleAddFolder}>
<SelectableListItem
itemId={NavigationMenuItemType.FOLDER}
onEnter={isAddingToFolder ? undefined : handleAddFolder}
>
<SidePanelItemWithAddToNavigationDrag
icon={IconFolder}
label={t`Folder`}
id="folder"
id={NavigationMenuItemType.FOLDER}
onClick={handleAddFolder}
dragIndex={3}
dragIndex={isDragDisabled ? undefined : 3}
payload={{
type: NavigationMenuItemType.FOLDER,
folderId: 'new',
name: t`New folder`,
}}
disabled={isAddingToFolder}
disableDrag={isDragDisabled}
/>
</SelectableListItem>
<SelectableListItem itemId="link" onEnter={handleAddLink}>
<SelectableListItem
itemId={NavigationMenuItemType.LINK}
onEnter={handleAddLink}
>
<SidePanelItemWithAddToNavigationDrag
icon={IconLink}
label={t`Link`}
id="link"
id={NavigationMenuItemType.LINK}
onClick={handleAddLink}
dragIndex={4}
dragIndex={isDragDisabled ? undefined : 4}
payload={{
type: NavigationMenuItemType.LINK,
linkId: 'new',
name: t`Link label`,
link: 'https://www.example.com',
}}
disableDrag={isDragDisabled}
/>
</SelectableListItem>
</SidePanelGroup>
@@ -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 (
<SidePanelSystemObjectPickerSubView
@@ -96,6 +105,7 @@ export const SidePanelNewSidebarItemObjectFlow = ({
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
emptyNoResultsText={t`All system objects are already in the sidebar`}
disableDrag={disableDrag}
/>
);
}
@@ -110,6 +120,7 @@ export const SidePanelNewSidebarItemObjectFlow = ({
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
disableDrag={disableDrag}
/>
);
};
@@ -1,17 +1,17 @@
import { Avatar } from 'twenty-ui/display';
import { Avatar, IconFolder, useIcons } from 'twenty-ui/display';
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
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 type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
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 { CoreObjectNameSingular } from 'twenty-shared/types';
type SearchRecord = {
recordId: string;
@@ -23,13 +23,15 @@ type SearchRecord = {
type SidePanelNewSidebarItemRecordItemProps = {
record: SearchRecord;
dragIndex?: number;
disableDrag?: boolean;
};
export const SidePanelNewSidebarItemRecordItem = ({
record,
dragIndex,
disableDrag = false,
}: SidePanelNewSidebarItemRecordItemProps) => {
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}
/>
</SelectableListItem>
@@ -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 = {
@@ -31,6 +33,10 @@ export const SidePanelNewSidebarItemRecordSubView = ({
onBack,
}: SidePanelNewSidebarItemRecordSubViewProps) => {
const { t } = useLingui();
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const disableDrag = addMenuItemInsertionContext?.disableDrag === true;
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const [recordSearchInput, setRecordSearchInput] = useState('');
@@ -105,7 +111,8 @@ export const SidePanelNewSidebarItemRecordSubView = ({
<SidePanelNewSidebarItemRecordItem
key={record.recordId}
record={record}
dragIndex={index}
dragIndex={disableDrag ? undefined : index}
disableDrag={disableDrag}
/>
))}
</SidePanelGroup>

Some files were not shown because too many files have changed in this diff Show More