Compare commits

...
Author SHA1 Message Date
Abdul Rahman 5f2918b178 feat: enhance navigation menu item icons with overlay support
- Added a new component, ObjectIconWithViewOverlay, to display icons with an overlay for navigation menu items.
- Updated CommandMenuNewSidebarItemViewPickerSubView to utilize the new overlay icon feature.
- Refactored NavigationMenuItemIcon to conditionally render the overlay based on item type.
- Modified NavigationDrawerItemForObjectMetadataItem to support the new icon rendering logic.
- Improved AddToNavigationDragHandle to accommodate custom icon content display.
2026-02-11 18:49:57 +05:30
Abdul Rahman 1eb863bfc0 feat: add icon support to navigation menu items
- Introduced an optional `icon` field to the `NavigationMenuItem` type in GraphQL schemas.
- Updated related input types and fragments to include the new `icon` field.
- Refactored components to utilize the new icon feature, including `CommandMenuFolderInfo` and `WorkspaceNavigationMenuItems`.
- Replaced the previous folder name update hook with a more versatile `useUpdateFolderInDraft` hook to handle both name and icon updates.
- Added a default folder icon constant for better management of folder icons across the application.
2026-02-11 18:30:47 +05:30
Abdul Rahman d545e05262 Merge branch 'navbar-customization-followup' into nav-folder-icon-customization 2026-02-11 16:21:59 +05:30
Abdul Rahman b04b7f953a Merge branch 'feat/navbar-customization' into navbar-customization-followup 2026-02-11 16:16:18 +05:30
Abdul Rahman 4d9b2d15e8 Merge branch 'main' into feat/navbar-customization 2026-02-11 16:15:58 +05:30
Abdul Rahman 5c7db7c887 refactor: optimize item type checks in CommandMenuObjectViewRecordInfo component
Refactored the logic for determining if the processed item is a view or record by using an array and the includes method for improved readability. Simplified the label assignment logic to enhance clarity. This change contributes to better maintainability and consistency in the CommandMenu components.
2026-02-11 16:11:59 +05:30
Abdul Rahman de032de7ee refactor: implement new sidebar item flows in CommandMenu components
Introduced new components for managing sidebar item creation flows, including CommandMenuNewSidebarItemObjectFlow and CommandMenuNewSidebarItemViewFlow. Updated CommandMenuNewSidebarItemMainMenu to utilize the new hooks for adding folders and links, enhancing the overall structure and maintainability of the CommandMenu. This refactor improves user experience by streamlining the process of adding new items to the navigation menu.
2026-02-11 15:29:52 +05:30
Abdul Rahman 5293fc81b4 refactor: update tests to use NavigationMenuItemType enum for item types
Replaced string literals with the NavigationMenuItemType enum in the getObjectMetadataForNavigationMenuItem tests. This change enhances type safety and consistency across the test suite, aligning with recent updates in the codebase.
2026-02-11 15:29:46 +05:30
Abdul RahmanandGitHub f530034288 Merge branch 'main' into feat/navbar-customization 2026-02-11 15:28:32 +05:30
Abdul Rahman 8cbd79aa4d refactor: implement new sidebar item flows in CommandMenu components
Introduced new components for managing sidebar item creation flows, including CommandMenuNewSidebarItemObjectFlow and CommandMenuNewSidebarItemViewFlow. Updated CommandMenuNewSidebarItemMainMenu to utilize the new hooks for adding folders and links, enhancing the overall structure and maintainability of the CommandMenu. This refactor improves user experience by streamlining the process of adding new items to the navigation menu.
2026-02-11 15:25:36 +05:30
Abdul Rahman 0c839475c3 refactor: update tests to use NavigationMenuItemType enum for item types
Replaced string literals with the NavigationMenuItemType enum in the getObjectMetadataForNavigationMenuItem tests. This change enhances type safety and consistency across the test suite, aligning with recent updates in the codebase.
2026-02-11 14:48:47 +05:30
Abdul Rahman 336b490520 Merge branch 'navbar-customization-followup' into nav-folder-icon-customization 2026-02-11 14:45:51 +05:30
Abdul Rahman fce5ce90e5 refactor: enhance normalizeUrl function to handle empty input
Updated the normalizeUrl function to return an empty string for empty or whitespace-only input. This change improves the function's robustness and ensures it handles edge cases more gracefully. Additionally, variable names were clarified for better readability.
2026-02-11 11:55:28 +05:30
Abdul Rahman e5862b9924 refactor: simplify folder picker logic in CommandMenu components
Updated the CommandMenuEditFolderPickerSubView and CommandMenuNavigationMenuItemEditPage components to streamline folder selection handling. Removed the useNavigationMenuItemEditSubView hook and replaced it with local state management for improved clarity and maintainability. The folder picker now directly manages its open/close state, enhancing the user experience and reducing complexity in the component structure.
2026-02-11 11:51:45 +05:30
Abdul Rahman d9da68bd3d refactor: standardize item type usage in CommandMenu components with NavigationMenuItemType enum
Updated various CommandMenu components and utilities to replace string literals for item types with the NavigationMenuItemType enum. This change enhances type safety, consistency, and maintainability across the codebase, reducing the risk of errors related to item type handling.
2026-02-11 11:43:16 +05:30
Abdul Rahman 2ddad5d9c7 refactor: update CommandMenu components to use NavigationMenuItemType enum
Replaced string literals for item types in CommandMenuFolderInfo and CommandMenuLinkInfo components with the NavigationMenuItemType enum for improved type safety and consistency across the codebase. This change enhances maintainability and reduces the risk of errors related to item type handling.
2026-02-11 11:39:07 +05:30
Abdul Rahman 8b455a8016 Merge branch 'feat/navbar-customization' into navbar-customization-followup 2026-02-11 11:36:44 +05:30
Abdul Rahman 7391b7b48b test: remove obsolete test files for navigation menu item utilities
Deleted outdated test files for getIconBackgroundColorForPayload, getNavigationMenuItemIconColors, and isWorkspaceDroppableId functions to streamline the test suite and eliminate redundancy. These tests are no longer necessary due to recent refactoring and updates in the utility functions.
2026-02-11 11:36:16 +05:30
Abdul Rahman 1a7b1ab077 refactor: improve code readability in tests and components
- Enhanced the formatting of test cases in recordIdentifierToObjectRecordIdentifier, sortNavigationMenuItems, and validateAndExtractWorkspaceFolderId tests for better clarity.
- Added comments to the CommandMenuNewSidebarItemViewPickerSubView component to clarify prop spreading, improving maintainability and understanding of the code.
2026-02-11 11:36:00 +05:30
Abdul Rahman 698074e377 refactor: remove unused feature flag from seedFeatureFlags utility
Eliminated the IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED feature flag from the seedFeatureFlags utility, streamlining the code and removing redundancy.
2026-02-11 11:31:20 +05:30
Abdul Rahman 9cd9e505f3 refactor: streamline CommandMenuNavigationMenuItemEditPage logic with switch statement
Refactored the CommandMenuNavigationMenuItemEditPage component to replace multiple if statements with a switch statement for improved readability and maintainability. This change enhances the handling of different navigation menu item types, ensuring clearer logic flow and reducing code duplication.
2026-02-11 11:19:35 +05:30
Abdul Rahman 28bd3707d4 Merge branch 'feat/navbar-customization' into navbar-customization-followup 2026-02-11 11:13:45 +05:30
Abdul Rahman 37978120d0 test: lower functions coverage threshold in Jest configuration
Reduced the functions coverage threshold in the Jest configuration from 48% to 40%, aligning with updated testing standards.
2026-02-11 11:13:19 +05:30
Abdul Rahman f44f47be1e refactor: enhance workspace folder ID validation in validateAndExtractWorkspaceFolderId utility
Updated the validateAndExtractWorkspaceFolderId utility to utilize the isNonEmptyString guard for improved validation of workspace folder IDs. This change enhances error handling by ensuring that only non-empty strings are accepted as valid folder IDs, contributing to better type safety and clarity in the navigation menu item logic.
2026-02-11 11:07:38 +05:30
Abdul Rahman e2bb09e992 refactor: replace NAVIGATION_MENU_ITEM_TYPE constant with NavigationMenuItemType enum
Refactored navigation menu item components and hooks to replace the deprecated NAVIGATION_MENU_ITEM_TYPE constant with a new NavigationMenuItemType enum for improved type safety and clarity. Updated all relevant imports and usages across the codebase to ensure consistency and maintainability.
2026-02-11 11:04:33 +05:30
Abdul Rahman fdb1aa461e refactor: replace NAVIGATION_MENU_ITEM_DROPPABLE_IDS constant with NavigationMenuItemDroppableIds enum
Refactored navigation menu item components and hooks to replace the NAVIGATION_MENU_ITEM_DROPPABLE_IDS constant with a new NavigationMenuItemDroppableIds enum for improved type safety and clarity. Updated all relevant imports and usages across the codebase to ensure consistency and maintainability.
2026-02-11 11:03:36 +05:30
Abdul Rahman 45297fd057 refactor: replace NAVIGATION_SECTIONS constant with NavigationSections enum
Refactored the navigation menu item components to replace the existing NAVIGATION_SECTIONS constant with a new NavigationSections enum for improved type safety and clarity. Updated related components and utility functions to utilize the new enum, ensuring consistency across the codebase. Removed the deprecated NavigationSectionId type as part of this transition.
2026-02-11 11:00:23 +05:30
Abdul Rahman 38e5d85287 refactor: update navigation menu item imports and introduce new utility functions
Refactored navigation menu item components and hooks to replace the old import paths for navigation sections with a new centralized import. Introduced two new utility functions, computeInsertIndexAndPosition and normalizeUrl, to enhance the management of navigation menu items and URL normalization. Added corresponding unit tests to ensure functionality and reliability of the new utilities.
2026-02-11 10:59:50 +05:30
Abdul Rahman f58bdd7245 feat: introduce CommandMenuNavigationItemActions enum and update related components
Added a new enum, CommandMenuNavigationItemActions, to centralize action identifiers for the command menu navigation items. Updated CommandMenuEditOrganizeActions and getOrganizeActionsSelectableItemIds to utilize this enum, enhancing code consistency and maintainability across the command menu functionality.
2026-02-11 10:55:30 +05:30
Abdul Rahman 547f623ee2 refactor: replace useSelectedNavigationMenuItemEditData with new hooks
Refactored multiple components and hooks to replace the deprecated useSelectedNavigationMenuItemEditData with more granular hooks: useSelectedNavigationMenuItemEditItem, useSelectedNavigationMenuItemEditItemLabel, and useSelectedNavigationMenuItemEditItemObjectMetadata. This change enhances code clarity and modularity, improving the management of selected navigation menu items across the command menu functionality.
2026-02-11 10:50:04 +05:30
Abdul Rahman 022f1f70d8 refactor: replace useNavigationMenuItemEditFolderData with useDraftNavigationMenuItems
Updated multiple components and hooks to utilize the new useDraftNavigationMenuItems hook, enhancing the management of draft navigation menu items. Removed the deprecated useNavigationMenuItemEditFolderData hook and adjusted related logic to ensure consistency across the command menu functionality.
2026-02-11 10:38:35 +05:30
Abdul Rahman cb50b65496 feat: add CommandMenuFolderInfo and CommandMenuLinkInfo components
Introduced two new components, CommandMenuFolderInfo and CommandMenuLinkInfo, to enhance the command menu functionality. These components allow users to edit folder names and link labels directly within the command menu, improving user experience and interaction. Updated CommandMenuPageInfo to integrate these new components based on the selected item type.
2026-02-11 10:28:53 +05:30
Abdul Rahman e3228fa467 test: lower coverage thresholds in Jest configuration
Adjusted the coverage thresholds in the Jest configuration to 48% for lines and 48% for functions, reflecting a revised standard for test coverage requirements.
2026-02-11 10:20:49 +05:30
Abdul Rahman fe96cc6d27 refactor: simplify drop handling logic in useHandleAddToNavigationDrop hook
Removed unnecessary checks for drop destination IDs in the useHandleAddToNavigationDrop hook. The logic now directly checks for defined folder IDs, streamlining the drop handling process and improving code readability.
2026-02-11 08:06:29 +05:30
Abdul Rahman 51b6effbd2 test: adjust coverage thresholds in Jest configuration
Updated the coverage thresholds in the Jest configuration to 48.6% for lines and 48.4% for functions, reflecting a revised standard for test coverage requirements.
2026-02-11 08:03:09 +05:30
Abdul Rahman 4d8046bc08 test: update coverage thresholds in jest configuration
Adjusted the coverage thresholds in the Jest configuration to improve code quality metrics. The new thresholds are set to 49.5% for statements, 49.5% for lines, and 49.4% for functions, reflecting a more stringent requirement for test coverage.
2026-02-11 07:49:18 +05:30
Abdul Rahman be1cae4a25 test: update navigation menu item utility tests for clarity and coverage
Refactored existing test cases for navigation menu item utilities to improve clarity and consolidate similar assertions. Key changes include:
- Simplified test descriptions for better understanding.
- Combined multiple assertions into single tests where applicable.
- Enhanced coverage for edge cases, ensuring robust validation of utility functions.

These updates aim to maintain the reliability of navigation menu item utilities while improving the overall readability of the test suite.
2026-02-11 07:49:10 +05:30
Abdul Rahman b9b8062d39 test: add unit tests for calculateNewPosition utility
Introduced a new test suite for the calculateNewPosition function, covering various scenarios including edge cases for moving items in a draggable list. The tests validate the correct position calculations when items are moved to the beginning, end, or within the list, ensuring reliable behavior of the drag-and-drop functionality.
2026-02-11 07:29:08 +05:30
Abdul Rahman 1420ebabbc refactor: enhance CommandMenuItemWithAddToNavigationDrag for improved icon handling and payload registration
- Updated the component to accept a new `customIconContent` prop for better icon customization.
- Refactored the payload registration logic to ensure it is registered on mouse events, improving drag-and-drop functionality.
- Cleaned up the code structure for better readability and maintainability.
2026-02-11 06:58:05 +05:30
Abdul Rahman bc93710e83 refactor: replace AddToNavigationIconSlot with AddToNavigationDragHandleIcon for improved icon handling
- Introduced AddToNavigationDragHandleIcon to manage custom icon content and standard icons more effectively.
- Removed the AddToNavigationIconSlot component to streamline the codebase.
- Updated CommandMenuNewSidebarItemRecordItem and AddToNavigationDragHandle components to utilize the new icon handling approach.
2026-02-11 06:54:43 +05:30
Abdul Rahman 7b2a5ffe76 Merge branch 'main' into feat/navbar-customization 2026-02-11 06:36:34 +05:30
Abdul Rahman 03971aa38a test: add unit tests for navigation menu item utilities
Introduced new test files for various utility functions related to navigation menu items. The tests cover the following functionalities:
- `getDropTargetIdFromDestination`: Validates the correct drop target ID generation based on different droppable IDs.
- `getIconBackgroundColorForPayload`: Ensures the correct background color is returned for different payload types.
- `getNavigationMenuItemIconColors`: Confirms the correct theme colors are returned for various navigation menu item types.
- `isWorkspaceDroppableId`: Tests the identification of workspace droppable IDs under various conditions.

These tests enhance the reliability of the navigation menu item utilities by ensuring expected behaviors are maintained.
2026-02-11 01:31:39 +05:30
Abdul Rahman c0bcbc1e01 Merge branch 'main' into feat/navbar-customization 2026-02-11 00:48:42 +05:30
Abdul Rahman 2c81170bf3 refactor: streamline state management in navigation components
Updated the `useCommandMenu`, `PageDragDropProvider`, and `useHandleAddToNavigationDrop` hooks to utilize the new `getSnapshotValue` utility for improved state access. This change enhances code clarity and consistency by reducing direct interactions with the Recoil state. Additionally, refactored the navigation drop handling logic to encapsulate repetitive code into a new function, `openEditForNewNavItem`, simplifying the process of opening edit modes for new navigation items.
2026-02-11 00:48:10 +05:30
Abdul Rahman e6d97717d4 feat: integrate Recoil state management for navigation drag-and-drop
Added a new Recoil atom `addToNavPayloadRegistryState` to manage the state of draggable items in the navigation menu. Updated the `CommandMenuItemWithAddToNavigationDrag` component to utilize this state for handling drag-and-drop operations. Refactored the drag update and drop handling logic in `PageDragDropProvider` and `useHandleAddToNavigationDrop` to leverage the new state management, improving the overall drag-and-drop functionality and ensuring better item tracking during interactions. Removed the deprecated utility functions related to draggable IDs.
2026-02-11 00:39:38 +05:30
Abdul Rahman 01bb4214ad feat: implement drag-and-drop functionality for navigation menu items
Added new components `CommandMenuAddToNavDraggablePlaceholder` and `CommandMenuAddToNavDroppable` to facilitate drag-and-drop interactions within the navigation menu. Updated existing components to integrate these new features, allowing users to rearrange items more intuitively. Enhanced the `CommandMenuItemWithAddToNavigationDrag` to support drag indices for better item positioning during drag operations. Refactored related components to ensure compatibility with the new drag-and-drop context.
2026-02-10 21:22:05 +05:30
Abdul Rahman 584f13f0d4 Merge branch 'main' into feat/navbar-customization 2026-02-10 20:08:57 +05:30
Abdul Rahman 7889fc65e0 refactor: update navigation menu item matching logic
Replaced 'objectNameSingular' with 'itemType' in the isLocationMatchingNavigationMenuItem utility to improve clarity and consistency. Updated corresponding tests to reflect this change, ensuring accurate navigation item matching based on item types.
2026-02-10 19:55:03 +05:30
Abdul Rahman aefe6f67fc refactor: improve navigation menu item movement logic
Updated the logic for moving navigation menu items to account for folder structure. The changes ensure that items are moved within their respective folders, enhancing the accuracy of item positioning. This includes adjustments to how siblings are identified and managed during move operations.
2026-02-10 19:48:44 +05:30
Abdul Rahman 5b16cde53f Merge branch 'main' into feat/navbar-customization 2026-02-10 18:15:07 +05:30
Abdul Rahman fcb5d7ee20 fix: simplify folder item handling in useWorkspaceSectionItems hook
Refactored the useWorkspaceSectionItems hook to always push folder items into the accumulator, removing the conditional check for defined folder children. This change streamlines the logic for handling navigation menu items, ensuring that all folder items are consistently processed.
2026-02-10 16:50:04 +05:30
Abdul Rahman 90de7f818b test: add unit tests for recordIdentifierToObjectRecordIdentifier utility function
Introduced unit tests for the recordIdentifierToObjectRecordIdentifier function, validating its behavior with various input scenarios. The tests ensure correct mapping of record identifiers to object records, including handling of avatar URLs and link generation for specific object types, enhancing overall code reliability.
2026-02-10 15:44:26 +05:30
Abdul Rahman 58bc4c4184 test: add unit tests for workspace navigation menu item utilities
Introduced unit tests for the filterWorkspaceNavigationMenuItems and getObjectMetadataForNavigationMenuItem utility functions. These tests validate the filtering of navigation menu items based on userWorkspaceId and ensure correct retrieval of object metadata for various item types, enhancing overall code reliability.
2026-02-10 15:35:33 +05:30
Abdul Rahman ea8c91002d fix: update navigation menu item error messages to include external link option
Enhanced the error messages for navigation menu item creation to specify that an external link is now a valid option. This change improves user guidance by clarifying the requirements for creating navigation menu items.
2026-02-10 15:03:23 +05:30
Abdul Rahman 7bca98fadb test: add unit tests for navigation menu item utilities
Introduced comprehensive unit tests for utility functions related to navigation menu items, including normalization of URLs, computation of insert indices and positions, and validation of workspace folder IDs. These tests enhance code reliability and ensure correct functionality across various scenarios.
2026-02-10 15:01:19 +05:30
Abdul Rahman e5acbcf555 fix: add link property to navigation menu item metadata
Included a link property set to null in various navigation menu item metadata creation functions. This change ensures that the link attribute is consistently defined across different components, improving data structure integrity and preparing for future enhancements.
2026-02-10 14:41:48 +05:30
Abdul Rahman b61377c29a fix: refine icon type validation in AddToNavigationIconSlot component
Updated the AddToNavigationIconSlot component to check for valid icon types by allowing only strings, numbers, and booleans to return null. This change enhances the component's robustness by ensuring that only appropriate icon types are processed, preventing potential rendering issues.
2026-02-10 14:35:28 +05:30
Abdul Rahman 655ec251af fix: include folderUniversalIdentifier in navigation menu item metadata
Added folderUniversalIdentifier to the buildStandardFlatNavigationMenuItemMaps and createStandardNavigationMenuItemFolderFlatMetadata functions. This enhancement ensures that the folder's unique identifier is correctly incorporated into the navigation menu item metadata, improving data integrity and consistency across the application.
2026-02-10 14:26:29 +05:30
Abdul Rahman 4f55adb73c fix: update folder content drop disabled logic in CurrentWorkspaceMemberNavigationMenuItems component
Modified the folderContentDropDisabled assignment to use isWorkspaceFolder, ensuring accurate handling of drop disabled states based on the current workspace context. This change improves the component's functionality and responsiveness to workspace conditions.
2026-02-10 14:21:43 +05:30
Abdul Rahman 7c92aeff20 fix: handle non-function icon types in AddToNavigationIconSlot component
Updated the AddToNavigationIconSlot component to return null when the icon prop is not a function. This change improves the component's robustness by ensuring that only valid icon components are rendered, preventing potential runtime errors.
2026-02-10 14:20:25 +05:30
Abdul Rahman 1738fa08f9 fix: enhance search records handling in CommandMenuNewSidebarItemRecordSubView
Updated the searchRecords assignment to safely access nested properties in searchData, ensuring robust handling of potential undefined values. This change improves the reliability of the component when processing search results.
2026-02-10 14:18:53 +05:30
Abdul Rahman ab392e7b13 fix: include targetRecordId in navigation menu item comparison logic
Updated the comparison logic in the useSaveNavigationMenuItemsDraft hook to include targetRecordId, ensuring accurate detection of changes in navigation menu items. This enhancement improves the functionality of draft saving by considering all relevant identifiers.
2026-02-10 14:16:29 +05:30
Abdul Rahman 67707da339 refactor: optimize sorting of object metadata items in CommandMenu components
Refactored the sorting logic for active non-system object metadata items in both CommandMenuNewSidebarItemPage and useFilteredObjectMetadataItems hooks. The sorting now utilizes the spread operator to create a new array before sorting, improving code clarity and consistency across components.
2026-02-10 14:15:21 +05:30
Abdul Rahman 4d123759f1 refactor: update contextual description logic in CommandMenuItemWithAddToNavigationDrag
Modified the logic for displaying the contextual description in the CommandMenuItemWithAddToNavigationDrag component. The description now defaults to the provided description when not hovered, improving clarity and user experience.
2026-02-10 14:14:01 +05:30
Abdul Rahman a3e242bd8b fix: add key prop to CommandMenuEditLinkItemView for improved rendering
Added a key prop using selectedItem.id to the CommandMenuEditLinkItemView component to ensure proper rendering and reconciliation of list items in React. This change enhances performance and prevents potential rendering issues when the selected item changes.
2026-02-10 14:11:59 +05:30
Abdul Rahman fa9dba834f refactor: improve item type handling and component logic in CommandMenu
Refactored the CommandMenuNavigationMenuItemEditPage and related hooks to enhance item type checks and streamline component logic. Updated imports for better organization and clarity, ensuring consistent handling of navigation menu item types. This change simplifies the logic for rendering components based on selected item types, improving overall code readability and maintainability.
2026-02-10 14:08:37 +05:30
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
d24264a302 Potential fix for pull request finding 'Useless conditional'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-02-10 14:05:05 +05:30
Abdul Rahman b8a2600ba5 refactor: enhance folder selection logic in CommandMenu components
Refactored the CommandMenuEditFolderPickerSubView to utilize a new custom hook, useFolderPickerSelectionData, for improved folder selection management. This change simplifies the component's logic by centralizing folder filtering and selection handling. Additionally, updated other components to use a new utility function for generating selectable item IDs, enhancing code consistency and readability.
2026-02-10 14:04:21 +05:30
Abdul Rahman d2bf4a7e9a refactor: standardize navigation menu item type handling across components
Updated various components and hooks to utilize a centralized NAVIGATION_MENU_ITEM_TYPE constant for item type checks. This change enhances code consistency and readability by replacing string literals with a defined type, ensuring better maintainability and reducing the risk of errors in item type handling.
2026-02-10 13:50:15 +05:30
Abdul Rahman eaec7dedcc refactor: remove getNavigationMenuItemType utility and streamline item type handling
Eliminated the getNavigationMenuItemType utility function and replaced its usage with direct access to itemType properties in relevant components and hooks. This change simplifies the logic for determining navigation menu item types and enhances code clarity.
2026-02-10 13:41:37 +05:30
Abdul Rahman b7798efb4c refactor: simplify item type checks in CommandMenu components
Updated the CommandMenuEditFolderPickerSubView and CommandMenuNavigationMenuItemEditPage components to replace individual item type flags with a single selectedItemType property. This change streamlines the logic for determining item types and enhances code readability. Additionally, removed unused flags from the useSelectedNavigationMenuItemEditData hook.
2026-02-10 13:37:28 +05:30
Abdul Rahman 6b27902df2 feat: add 'link' property to entity properties configuration
Introduced a new 'link' property in the ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME constant to enhance entity metadata configuration.
2026-02-10 13:21:04 +05:30
Abdul Rahman 4b0bd71e02 Merge branch 'main' into feat/navbar-customization 2026-02-10 13:20:50 +05:30
Abdul Rahman 2efe056977 Add Link and Icon Properties to Navigation Menu Item Migrations
- Updated the migration command to include `link` and `icon` properties for navigation menu items, ensuring consistency across various components.
- Enhanced metadata creation utilities to support these properties, improving the overall functionality and representation of navigation menu items.
2026-02-10 10:43:02 +05:30
Abdul Rahman d21e15556b Remove migration for adding link to navigation menu item
- Deleted the migration file that added a `link` column to the `navigationMenuItem` table, as it is no longer needed following recent updates to the database schema.
2026-02-10 10:36:25 +05:30
Abdul Rahman 5fc9c725b6 Add Link and Icon Columns to Navigation Menu Item Table
- Created a new migration to add `link` and `icon` columns to the `navigationMenuItem` table in the database.
- This enhancement supports the recent updates to navigation menu item components, allowing for better representation and functionality.
2026-02-10 10:27:42 +05:30
Abdul Rahman f06ce683d4 Add Link and Icon Properties to Entity Properties Configuration
- Introduced `link` and `icon` properties to the `ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME` constant, enhancing the metadata structure for better representation and functionality.
- Both properties are set to not be stringified and have undefined universal properties, maintaining consistency with existing configurations.
2026-02-10 10:19:49 +05:30
Abdul Rahman 2f1d6f8b7e Merge branch 'main' into feat/navbar-customization 2026-02-10 10:19:35 +05:30
Abdul Rahman 2ae6c52e4b Add Icon Property to Navigation Menu Item Components
- Introduced an `icon` property across various navigation menu item components, including input types, DTOs, and entities, to enhance the representation of menu items.
- Updated utility functions to accommodate the new `icon` property, ensuring consistent handling during item creation and transformation.
- Enhanced metadata creation utilities to support the inclusion of icons, improving the overall functionality and user experience of the navigation menu.
2026-02-10 10:15:24 +05:30
Abdul Rahman 919766cced Add CommandMenuObjectViewRecordInfo Component for Enhanced View and Record Display
- Introduced `CommandMenuObjectViewRecordInfo` component to display information for selected view and record items in the command menu.
- Updated `CommandMenuPageInfo` to integrate the new component, improving the handling of view and record types.
- Enhanced `useSelectedNavigationMenuItemEditData` hook to include a flag for record items, supporting the new component's functionality.
2026-02-10 00:50:13 +05:30
Abdul Rahman b048b7a19d Add Add Before and Add After Functionality to Command Menu Components
- Introduced `onAddBefore` and `onAddAfter` props in `CommandMenuEditLinkItemView`, `CommandMenuEditObjectViewBase`, and `CommandMenuEditOrganizeActions` to support adding items before and after existing menu items.
- Updated `CommandMenuNavigationMenuItemEditPage` to include new actions for adding items, enhancing the command menu's functionality.
- Implemented context management for item insertion using Recoil, allowing for dynamic placement of new items in the navigation menu.
- Added new icons for the add actions to improve visual representation in the command menu.
- Created `addMenuItemInsertionContextState` and `AddMenuItemInsertionContext` types to manage the state related to item insertion, improving code organization and maintainability.
2026-02-10 00:40:12 +05:30
Abdul Rahman a95f15274e Refactor Command Menu New Sidebar Item Components for Improved Logic and Clarity
- Removed unused variables and props in `CommandMenuNewSidebarItemPage` and `CommandMenuNewSidebarItemViewPickerSubView`, streamlining the component logic.
- Simplified filtering logic for available object metadata items, enhancing performance and readability.
- Updated the structure of the components to improve maintainability and user experience in the command menu.
2026-02-10 00:21:31 +05:30
Abdul Rahman 22469701ae Refactor WorkspaceNavigationMenuItemsFolder for Improved Padding Logic
- Updated `StyledFolderDroppableContent` to replace `$isEditMode` and `$isEmpty` props with a single `$compact` prop, simplifying the padding logic based on the editing state and item presence.
- Adjusted the `WorkspaceNavigationMenuItemsFolder` component to utilize the new `$compact` prop, enhancing code clarity and maintainability.
2026-02-10 00:15:38 +05:30
Abdul Rahman 326f32e62f Refactor WorkspaceNavigationMenuItemsFolder for Enhanced Styling and Logic
- Updated `StyledFolderDroppableContent` to accept new props `$isEditMode` and `$isEmpty`, allowing for dynamic padding adjustments based on the editing state and item presence.
- Integrated these props into the `WorkspaceNavigationMenuItemsFolder` component to improve layout responsiveness during editing, enhancing user experience.
2026-02-09 23:31:12 +05:30
Abdul Rahman 09bf955538 Refactor Command Menu Components to Streamline Object Editing and Remove Unused Subviews
- Simplified `CommandMenuEditObjectViewBase` by removing unnecessary props and components, enhancing clarity and maintainability.
- Deleted `CommandMenuEditViewPickerSubView` and related hooks to streamline the command menu structure, reducing complexity.
- Updated `CommandMenuNavigationMenuItemEditPage` to reflect changes in the object editing logic, improving overall functionality and user experience.
2026-02-09 23:10:52 +05:30
Abdul Rahman a113468440 Refactor Navigation Menu Components to Enhance Navigation Logic
- Updated `CurrentWorkspaceMemberNavigationMenuItems` and `WorkspaceNavigationMenuItemsFolder` components to utilize `useNavigate` for improved navigation handling.
- Introduced logic to navigate to the first non-link item when the menu is closed, enhancing user experience and streamlining navigation.
- Refactored location handling to improve code clarity and maintainability across components.
2026-02-09 22:42:17 +05:30
Abdul Rahman 5b55f915d5 Enhance Navigation Item Drop Target and Workspace Menu with Add Item Functionality
- Updated the `NavigationItemDropTarget` component to support a new `compact` prop, allowing for dynamic height adjustment based on the editing state.
- Introduced `handleAddMenuItem` function in `WorkspaceNavigationMenuItems` to facilitate adding new items to the navigation menu.
- Enhanced `NavigationDrawerSectionForWorkspaceItems` to include an `onAddMenuItem` prop, enabling the display of an "Add menu item" option when in edit mode, improving user interaction and navigation management.
2026-02-09 22:37:22 +05:30
Abdul Rahman 26e67db0aa Update CommandMenuSubViewWithSearch styling for improved visual consistency
- Changed the border color in the StyledSearchContainer from medium to light, enhancing the overall appearance and alignment with the theme's design principles.
2026-02-09 22:26:29 +05:30
Abdul Rahman af1fcee234 Enhance Command Menu New Sidebar Item with Submenu Support
- Added a new `hasSubMenu` prop to the `CommandMenuNewSidebarItemViewObjectPickerSubView` component, enabling the display of submenu items for better organization and navigation.
- Updated the component's structure to improve user interaction and enhance the overall functionality of the command menu.
2026-02-09 22:26:07 +05:30
Abdul Rahman f9df7b87f8 Enhance Command Menu Sidebar Item with Displayable Views and System Object Option
- Added logic to filter object metadata items based on displayable views, improving the selection process in the command menu.
- Introduced a new prop `showSystemObjectsOption` to conditionally render the system objects option in the sidebar, enhancing user experience.
- Updated related components to utilize the new filtering and display logic, ensuring a more intuitive interaction with the command menu items.
2026-02-09 22:12:08 +05:30
Abdul Rahman 6f78f9a796 Implement Navigation Sections and Enhance Drag-and-Drop Functionality
- Introduced `NAVIGATION_SECTIONS` constants to categorize navigation items into 'workspace' and 'favorites', improving code clarity and organization.
- Added `NavigationDragSourceContext` to manage the source droppable ID during drag-and-drop operations, enhancing state management.
- Updated various components to utilize the new context and constants, including `NavigationItemDropTarget`, `NavigationMenuItemDroppable`, and others, to improve drag-and-drop handling.
- Implemented logic to disable drop targets based on the current section, enhancing user experience during drag-and-drop interactions.
- These changes collectively improve the structure and functionality of the navigation menu, ensuring a more intuitive drag-and-drop experience.
2026-02-09 21:55:32 +05:30
Abdul Rahman 8f7b189717 Enhance Drag Preview Functionality with Recoil Integration
- Updated the `createAddToNavigationDragPreview` function to wrap the `AddToNavigationDragPreview` component in a `RecoilRoot`, enabling state management for drag-and-drop operations.
- Adjusted the positioning of the drag preview element from off-screen to the top-left corner, improving visibility during drag actions.
- These changes enhance the functionality and user experience of the drag-and-drop feature within the navigation menu.
2026-02-09 21:07:42 +05:30
Abdul Rahman e2da06cf3c Refactor NavigationSidebarNativeDropZone for Improved Drag-and-Drop Handling
- Renamed drag event handlers for clarity, changing `handleDocumentDrop` to `handleDocumentDragStart` and introducing `handleDocumentDragEnd` to manage drag state more effectively.
- Enhanced the logic for setting active drop targets based on the current draft items, improving user feedback during drag-and-drop operations.
- Updated event listener management to ensure proper cleanup, contributing to better performance and maintainability of the component.
- These changes collectively enhance the drag-and-drop experience within the navigation sidebar.
2026-02-09 20:16:02 +05:30
Abdul Rahman 92ba245e87 Refactor AddToNavigationDragHandle and IconWithBackground for Improved Icon Sizing
- Updated the `StyledIconSlot` component to use a `$hasFixedSize` prop instead of `$hasBackgroundColor`, enhancing the flexibility of the drag handle's appearance.
- Simplified icon size handling in `AddToNavigationDragHandle` and `IconWithBackground` components by standardizing the size to `theme.icon.size.md`, improving consistency across the application.
- These changes enhance the visual coherence of the navigation menu items and streamline icon rendering logic.
2026-02-09 19:52:07 +05:30
Abdul Rahman 5caa8df0cf Refactor Command Menu Components for Enhanced Icon Handling and Theming
- Introduced the `IconWithBackground` component to standardize icon rendering with background colors, improving visual consistency across command menu items.
- Updated `CommandMenuItem`, `CommandMenuObjectMenuItem`, and related components to utilize the new `IconWithBackground`, enhancing their appearance and theming capabilities.
- Integrated theme-based icon color management in `CommandMenuNewSidebarItemViewObjectPickerSubView` and `CommandMenuNewSidebarItemViewSystemSubView`, ensuring better alignment with the overall design.
- These changes collectively improve the user interface and maintainability of the command menu components.
2026-02-09 19:50:14 +05:30
Abdul Rahman d8292062cc Refactor AddToNavigationDragHandle Component for Enhanced Customization
- Updated the `StyledIconSlot` to accept an optional `$backgroundColor` prop, allowing for greater customization of the drag handle's appearance.
- Simplified the cursor handling logic and adjusted padding and width based on the presence of the background color, improving the component's flexibility.
- Enhanced the `AddToNavigationDragHandle` props to include `iconBackgroundColor`, providing more control over the icon's styling during drag operations.
- These changes improve the usability and visual consistency of the drag handle within the navigation menu.
2026-02-09 18:43:57 +05:30
Abdul Rahman 439d81c8d7 Enhance Command Menu Item with Contextual Drag Description
- Integrated the `useLingui` hook to provide internationalization support for the drag-and-drop functionality within the `CommandMenuItemWithAddToNavigationDrag` component.
- Updated the description displayed during hover to show a contextual message, improving user guidance when dragging items to the navigation bar.
- These changes enhance the user experience by providing clearer instructions during drag operations.
2026-02-09 18:23:39 +05:30
Abdul Rahman fae016b6f9 Refactor AddToNavigationIconSlot Component for Improved Icon Handling
- Simplified the icon rendering logic by replacing the custom `isIconComponent` type guard with `isValidElement` to streamline the component's functionality.
- Enhanced the handling of icon components to ensure proper rendering based on their type, improving code clarity and maintainability.
- These changes contribute to a more efficient and understandable implementation of the AddToNavigationIconSlot component.
2026-02-09 17:15:22 +05:30
Abdul Rahman 8159a0cede Update Command Menu Icons for Improved Clarity
- Replaced `IconFolderPlus` with `IconFolderSymlink` in the `CommandMenuEditOrganizeActions` component to better represent the action of moving items to a folder.
- Added `IconFolderSymlink` to the `twenty-ui` display module, ensuring it is available for use across the application.
- These changes enhance the visual representation of actions within the command menu, improving user experience and clarity.
2026-02-09 17:08:02 +05:30
Abdul Rahman 2b465f5b52 Refactor Workspace Navigation Menu Items Folder for Enhanced Structure and Functionality
- Introduced a new styled component, `StyledFolderExpandableWrapper`, to improve layout management during drag-and-drop operations.
- Updated the rendering logic within `WorkspaceNavigationMenuItemsFolder` to enhance clarity and maintainability by utilizing the new styled component.
- Simplified the handling of navigation menu items by directly integrating the `Droppable` component, ensuring a more efficient drag-and-drop experience.
- These changes collectively enhance the user experience and maintain the integrity of the navigation menu during interactions.
2026-02-09 14:24:30 +05:30
Abdul Rahman 19472a9bb5 Add WorkspaceNavigationMenuItemFolderDragClone Component for Drag-and-Drop Functionality
- Introduced the `WorkspaceNavigationMenuItemFolderDragClone` component to enhance the drag-and-drop experience within the workspace navigation menu.
- Integrated the new component into the `WorkspaceNavigationMenuItemsFolder` to render a clone of the draggable item, improving user interaction during drag operations.
- This addition streamlines the drag-and-drop functionality, providing visual feedback and maintaining the integrity of the navigation menu items during dragging.
2026-02-09 14:16:24 +05:30
Abdul Rahman 84c804394d Refactor NavbarDragProvider to Improve Parameter Naming
- Updated the parameter name in the `handleDragStart` function from `_` to `_dragStart` for better clarity and understanding of its purpose.
- This change enhances code readability and maintainability by providing a more descriptive parameter name.
2026-02-09 14:13:24 +05:30
Abdul Rahman a3bfa3bba1 Refactor Workspace Navigation Menu Items Folder Component for Improved Structure
- Removed the unused `NavigationItemDropTarget` import to streamline the component.
- Introduced a new styled component, `StyledFolderDroppableContent`, to enhance layout consistency.
- Simplified the rendering logic by directly using `NavigationDrawerItem` and `DraggableItem`, improving code clarity and maintainability.
- These changes contribute to a more organized and efficient implementation of the workspace navigation menu items.
2026-02-09 13:07:15 +05:30
Abdul Rahman aaed4215b9 Implement Navbar Drag Provider and Refactor Navigation Components for Drag-and-Drop Functionality
- Introduced the `NavbarDragProvider` component to manage drag-and-drop context for navigation items, enhancing user interaction capabilities.
- Updated `MainNavigationDrawerScrollableItems` and `CurrentWorkspaceMemberFavoritesFolders` components to utilize the new drag provider, improving the organization of draggable items.
- Refactored navigation menu item components to replace the previous drag provider with a more streamlined approach, enhancing code clarity and maintainability.
- Adjusted constants for droppable IDs to support the new drag-and-drop logic, ensuring consistency across the navigation menu items.
- These changes collectively enhance the user experience by enabling intuitive drag-and-drop functionality within the navigation drawer.
2026-02-09 12:55:57 +05:30
Abdul Rahman 44602fc2cc Enhance Navigation Drawer Section Title Component with Always Visible Right Icon
- Added a new prop `alwaysShowRightIcon` to the `NavigationDrawerSectionTitle` component, allowing the right icon to remain visible regardless of mobile state.
- Updated the `StyledRightIcon` component to conditionally render opacity based on the new prop, improving user experience and accessibility.
- These changes enhance the flexibility and usability of the navigation drawer component.
2026-02-09 12:45:43 +05:30
Abdul Rahman 55d55a62c5 Update Icon Sizes and Container Dimensions for Consistency
- Adjusted icon sizes in multiple components to use a consistent spacing value of `3.5`, enhancing visual uniformity across the application.
- Updated the dimensions of the `StyledNavigationMenuItemIconContainer` to `4.5`, ensuring alignment with the new icon sizing.
- These changes improve the overall aesthetic and maintainability of the UI components.
2026-02-09 12:09:50 +05:30
Abdul Rahman 4faee658f0 Remove Unused Import in useSelectedNavigationMenuItemEditData Hook
- Eliminated the unused `isDefined` import from the `useSelectedNavigationMenuItemEditData` hook, enhancing code cleanliness and maintainability.
- This change contributes to a more organized codebase by removing unnecessary dependencies.
2026-02-09 12:02:00 +05:30
Abdul Rahman 160f7ca574 Refactor Workspace Navigation Menu Item Filtering for Enhanced Type Safety
- Updated the `filterWorkspaceNavigationMenuItems` function to utilize the `NavigationMenuItem` type, improving type consistency and clarity in filtering logic.
- This change enhances the maintainability and readability of the code by ensuring that the function operates on a well-defined type.
2026-02-09 11:57:58 +05:30
Abdul Rahman ab4262844b Refactor Navigation Menu Item Components for Enhanced Type Consistency
- Updated various components and hooks to utilize the new `itemType` property for improved clarity in navigation menu item handling.
- Removed unnecessary imports and simplified condition checks, enhancing code readability and maintainability.
- These changes contribute to a more organized and efficient implementation of navigation menu item logic.
2026-02-09 11:56:45 +05:30
Abdul Rahman 195bdac620 Refactor Navigation Menu Item Logic for Improved Type Handling
- Updated condition checks in `useSelectedNavigationMenuItemEditData`, `WorkspaceNavigationMenuItemsFolder`, and `useWorkspaceSectionItems` to utilize the new `itemType` property for better clarity and consistency.
- Removed unnecessary imports and simplified logic related to navigation menu item types, enhancing code readability and maintainability.
- These changes contribute to a more organized and efficient implementation of navigation menu item handling.
2026-02-08 20:53:20 +05:30
Abdul Rahman ef57d5c96e Enhance Navigation Menu Item Type Handling and Sorting Logic
- Introduced a new `NavigationMenuItemType` type to categorize menu items as 'folder', 'link', 'object', 'record', or 'view'.
- Updated the `ProcessedNavigationMenuItem` type to include an `itemType` property, improving clarity in item categorization.
- Refactored the `sortNavigationMenuItems` function to assign the appropriate `itemType` based on the item being processed, enhancing the sorting logic and maintainability.
- Simplified condition checks in `NavigationDrawerItemForObjectMetadataItem` to utilize the new `itemType` property for determining item characteristics, improving code readability.
2026-02-08 20:51:41 +05:30
Abdul Rahman f1f0f743ab Refactor StyledIcon Component for Improved CSS Handling
- Updated the CSS syntax in the `StyledIcon` component to use the `css` template literal for better readability and maintainability.
- This change enhances the clarity of the styling logic within the component, contributing to a more organized codebase.
2026-02-08 20:42:30 +05:30
Abdul Rahman f16b724419 Enhance Icon Component Handling in AddToNavigationIconSlot
- Updated the `isIconComponent` function to allow for both function and object types, improving flexibility in icon handling.
- Renamed the local variable from `IconComponent` to `Icon` for clarity in rendering.
- These changes contribute to a more robust and maintainable implementation of the AddToNavigationIconSlot component.
2026-02-08 20:41:18 +05:30
Abdul Rahman cb21655afd Refactor Navigation Menu Edit Mode Logic for Improved State Management
- Replaced the `useNavigationMenuEditModeActions` hook with direct state management using Recoil's `useSetRecoilState` in `NavigationMenuEditModeBar` and `WorkspaceNavigationMenuItems` components, enhancing clarity and modularity.
- Introduced a new `cancelEditMode` function to handle the cancellation of edit mode, improving the organization of state updates.
- Removed the now-unnecessary `useNavigationMenuEditModeActions` hook, streamlining the codebase and reducing complexity.
- These changes contribute to a more efficient and maintainable implementation of navigation menu edit mode functionality.
2026-02-08 20:20:06 +05:30
Abdul Rahman c12875eddd Refactor Navigation Menu Item Drag and Drop Logic for Improved Clarity
- Simplified the calculation of new positions in the `useHandleNavigationMenuItemDragAndDrop` hook by removing unnecessary rounding, enhancing code readability.
- Updated the `calculateNewPosition` utility to consistently return rounded values, improving the accuracy of position calculations during drag and drop operations.
- These changes contribute to a more efficient and maintainable implementation of drag and drop functionality in the navigation menu.
2026-02-08 14:10:40 +05:30
Abdul Rahman eddf0e11a5 Refactor Command Menu Item Hooks and Components for Improved Draft Management
- Replaced the `useUpdateNavigationMenuItemsDraft` hook with more specific hooks: `useUpdateFolderNameInDraft`, `useUpdateLinkInDraft`, and `useUpdateObjectInDraft`, enhancing clarity and modularity.
- Updated components to utilize the new hooks, streamlining draft management for folders, links, and objects.
- Introduced new hooks for adding items to the navigation menu draft, improving the organization and maintainability of the codebase.
- These changes contribute to a more efficient and structured implementation of command menu item handling.
2026-02-08 14:08:28 +05:30
Abdul Rahman dce2e0a0cc Refactor Command Menu Item Components to Standardize Icon Handling
- Updated `CommandMenuItemWithAddToNavigationDrag` and related components to replace the `Icon` prop with a unified `icon` prop, allowing for both `IconComponent` and `ReactNode` types.
- Introduced `AddToNavigationIconSlot` to encapsulate icon rendering logic, improving code clarity and reusability.
- These changes enhance the consistency and maintainability of the command menu item components.
2026-02-08 13:59:38 +05:30
Abdul Rahman ea52343c5d Refactor CommandMenuNavigationMenuItemEditPage for Enhanced Rendering Logic and Clarity
- Streamlined the rendering logic by consolidating condition checks for object, link, and folder items, improving code clarity and reducing redundancy.
- Removed unnecessary props and simplified the return statements for better maintainability.
- These changes contribute to a more organized and efficient implementation of the CommandMenuNavigationMenuItemEditPage component.
2026-02-08 13:48:59 +05:30
Abdul Rahman 0956c72da1 Refactor Command Menu Components for Improved Data Handling and Clarity
- Updated `CommandMenuEditViewPickerSubView` to utilize local state for managing `currentDraft` and `objectMetadataItems`, enhancing clarity and reducing reliance on external props.
- Simplified the `CommandMenuNavigationMenuItemEditPage` by removing unnecessary props and streamlining the rendering logic for improved maintainability.
- Enhanced `CommandMenuNewSidebarItemPage` and related components by consolidating draft handling and removing unused imports, contributing to a more organized implementation of the command menu components.
2026-02-08 13:41:15 +05:30
Abdul Rahman 9af54c53a4 Refactor CommandMenuNewSidebarItem Components for Enhanced Structure and Reusability
- Simplified the rendering logic in `CommandMenuNewSidebarItemPage` by removing unnecessary props from `CommandMenuNewSidebarItemRecordSubView`, improving clarity.
- Introduced `CommandMenuNewSidebarItemRecordItem` to encapsulate record item rendering, enhancing reusability and maintainability.
- Updated `CommandMenuNewSidebarItemRecordSubView` to utilize the new `CommandMenuNewSidebarItemRecordItem`, streamlining the component structure.
- These changes contribute to a more organized and efficient implementation of the command menu components.
2026-02-08 13:38:21 +05:30
Abdul Rahman 409e789c14 Refactor CommandMenuNavigationMenuItemEditPage for Improved Rendering Logic
- Simplified the rendering logic in `CommandMenuNavigationMenuItemEditPage` by consolidating the object view rendering into a single inline function, enhancing clarity and reducing redundancy.
- Updated the condition checks for rendering the object view, streamlining the component's structure and improving maintainability.
- These changes contribute to a more organized and efficient implementation of the CommandMenuNavigationMenuItemEditPage component.
2026-02-08 13:25:02 +05:30
Abdul Rahman c9c7fc176e Refactor CommandMenuEditViewPickerSubView for Enhanced View Selection Logic
- Updated `CommandMenuEditViewPickerSubView` to integrate local handling of view selection, improving clarity and reducing reliance on external props.
- Introduced a new `handleSelectView` function to streamline view selection and state management.
- Utilized `useRecoilValue` for managing the selected navigation menu item in edit mode, enhancing state management and code maintainability.
- These changes contribute to a more organized and efficient implementation of the CommandMenuEditViewPickerSubView component.
2026-02-08 13:24:51 +05:30
Abdul Rahman fe19af45da Refactor CommandMenuEditFolderPickerSubView for Enhanced Folder Selection Logic
- Updated `CommandMenuEditFolderPickerSubView` to integrate folder selection handling directly within the component, improving clarity and reducing reliance on external props.
- Removed the `onSelectFolder` prop and replaced it with a local `handleSelectFolder` function to streamline folder selection and state management.
- These changes enhance the organization and maintainability of the CommandMenuEditFolderPickerSubView component.
2026-02-08 13:18:27 +05:30
Abdul Rahman f82bcb4c16 Refactor Command Menu Components to Enhance State Management and Clarity
- Updated `CommandMenuEditFolderPickerSubView` and `CommandMenuNavigationMenuItemEditPage` to utilize `useRecoilValue` for managing the selected navigation menu item in edit mode, improving state management.
- Simplified the retrieval of selected item data by restructuring the hooks, enhancing code clarity and maintainability.
- These changes contribute to a more organized and efficient implementation of command menu components.
2026-02-08 13:16:32 +05:30
Abdul Rahman b5d91817cc Refactor CommandMenuNavigationMenuItemEditPage for Improved Action Handling
- Simplified the usage of `useNavigationMenuItemEditOrganizeActions` by destructuring its properties directly in the component.
- Updated the component to use the destructured properties for action handling, enhancing code clarity and reducing redundancy.
- These changes contribute to a more organized and maintainable implementation of the CommandMenuNavigationMenuItemEditPage component.
2026-02-08 13:10:23 +05:30
Abdul Rahman 7f9128a720 Refactor Navigation Menu Item Hooks and Components for Enhanced State Management
- Updated `CommandMenuNewSidebarItemPage`, `useNavigationMenuItemEditFolderData`, and `NavigationSidebarNativeDropZone` to utilize `useRecoilValue` for improved state management of `navigationMenuItemsDraft`.
- Simplified the retrieval of navigation menu items draft state across components, enhancing code clarity and maintainability.
- These changes contribute to a more organized and efficient implementation of navigation menu item handling.
2026-02-08 13:08:59 +05:30
Abdul Rahman 9456ac38dd Refactor Command Menu Components for Improved Structure and Reusability
- Removed unused imports and consolidated rendering logic in `CommandMenuNavigationMenuItemEditPage` and `CommandMenuNewSidebarItemPage` to enhance clarity.
- Introduced `CommandMenuObjectPickerItem` to standardize object menu item rendering across different components, improving code reusability.
- Updated `CommandMenuObjectPickerSubView` and `CommandMenuSystemObjectPickerSubView` to utilize the new `CommandMenuObjectPickerItem`, streamlining the rendering process.
- These changes enhance the organization, maintainability, and readability of the command menu components.
2026-02-07 00:56:52 +05:30
Abdul Rahman 5241c5a297 Refactor CommandMenu Components for Enhanced Data Management and Readability
- Introduced hooks `useNavigationMenuItemEditFolderData` and `useSelectedNavigationMenuItemEditData` to streamline data handling in `CommandMenuEditFolderPickerSubView`, `CommandMenuEditOwnerSection`, and `CommandMenuEditViewPickerSubView`.
- Simplified state management and search functionality within `CommandMenuEditFolderPickerSubView` by utilizing local state for search input.
- Improved the logic for determining application IDs in `CommandMenuEditOwnerSection` based on the current draft and selected item.
- These changes enhance the organization, maintainability, and clarity of the command menu components.
2026-02-07 00:37:10 +05:30
Abdul Rahman ed42de3508 Refactor CommandMenuNavigationMenuItemEditPage and Related Hooks for Enhanced Structure and Readability
- Introduced new hooks for managing navigation menu item edit data, including `useNavigationMenuItemEditFolderData`, `useNavigationMenuItemEditObjectPickerData`, `useNavigationMenuItemEditOrganizeActions`, `useNavigationMenuItemEditSubView`, and `useSelectedNavigationMenuItemEditData`.
- Simplified the CommandMenuNavigationMenuItemEditPage component by removing unused imports and consolidating state management logic.
- These changes improve the organization, maintainability, and clarity of the navigation menu item editing functionality.
2026-02-07 00:33:56 +05:30
Abdul Rahman 6ec8b57e39 Refactor CommandMenuNavigationMenuItemEditPage for Improved Readability
- Simplified subViewHandlers by converting them into individual functions for better clarity.
- Updated onBack and onOpen functions to directly reference the new individual handlers, enhancing code organization.
- These changes contribute to a more maintainable and readable implementation of the CommandMenuNavigationMenuItemEditPage component.
2026-02-07 00:05:08 +05:30
Abdul Rahman fa18caa41d Refactor CommandMenuEditFolderPickerSubView for Improved Logic and Readability
- Replaced the reduce method with a for-of loop for better clarity in the getDescendantFolderIds function.
- Enhanced the logic for accumulating descendant folder IDs, improving maintainability and readability.

These changes contribute to a more organized implementation of the CommandMenuEditFolderPickerSubView component.
2026-02-07 00:03:19 +05:30
Abdul Rahman 76ceaa34c7 Refactor Navigation Menu Hooks to Utilize New Filtering Utility
- Introduced the `filterWorkspaceNavigationMenuItems` utility to streamline the filtering of navigation menu items based on user workspace ID.
- Updated multiple hooks (`useNavigationMenuEditModeActions`, `useNavigationMenuItemsDraftState`, `usePrefetchedNavigationMenuItemsData`, and `useSaveNavigationMenuItemsDraft`) to use the new utility for improved code clarity and maintainability.
- These changes enhance the organization and readability of the navigation menu item handling logic.
2026-02-06 15:16:19 +05:30
Abdul Rahman c76d7ba96d Refactor useAddToNavigationMenuDraft for Improved Code Clarity
- Introduced helper functions `getMaxPosition` and `normalizeUrl` to streamline logic and enhance readability.
- Replaced inline logic with these helper functions in multiple locations to reduce code duplication.
- These changes contribute to a more organized and maintainable implementation of the useAddToNavigationMenuDraft hook.
2026-02-06 15:11:56 +05:30
Abdul Rahman 521d28db63 Refactor WorkspaceNavigationMenuItemsFolder for Improved Clarity and Structure
- Reorganized import statements for better clarity and consistency.
- Simplified the logic for updating open folder IDs to enhance readability.
- Updated the conditional check for edit mode click handling to ensure proper functionality.

These changes contribute to a more organized and maintainable implementation of the WorkspaceNavigationMenuItemsFolder component.
2026-02-06 15:07:49 +05:30
Abdul Rahman 02e1710319 Refactor NavigationSidebarNativeDropZone for Enhanced Readability
- Reorganized import statements for improved clarity and consistency.
- Updated the conditional check in the handleDrop function for better readability.

These changes contribute to a more structured and maintainable implementation of the NavigationSidebarNativeDropZone component.
2026-02-06 14:37:23 +05:30
Abdul Rahman e0b1f8b301 Refactor NavigationSidebarNativeDropZone for Improved Clarity and Structure
- Consolidated conditional rendering into a switch statement for better readability and maintainability.
- Updated the handling of folder and link additions to streamline the logic and enhance code organization.
- Reorganized import statements for improved clarity and consistency.

These changes contribute to a more efficient and structured implementation of the NavigationSidebarNativeDropZone component.
2026-02-06 14:36:46 +05:30
Abdul Rahman 5c12b0fe0c Refactor CommandMenuNewSidebarItemPage for Enhanced Rendering Logic
- Consolidated conditional rendering into a switch statement for improved clarity and maintainability.
- Streamlined the rendering logic for various sidebar item views, enhancing code organization.
- These changes contribute to a more efficient and structured implementation of the CommandMenuNewSidebarItemPage component.
2026-02-06 14:32:19 +05:30
Abdul Rahman 430f16eea0 Enhance NavigationMenuEditModeBar for Improved Theming and Clarity
- Integrated the `useTheme` hook to dynamically adjust icon sizes based on the theme.
- Updated the icon size assignment to utilize theme values, enhancing consistency in styling.
- These changes contribute to a more organized and visually coherent implementation of the NavigationMenuEditModeBar component.
2026-02-06 14:29:49 +05:30
Abdul Rahman 7268332aec Refactor NavigationItemDropTarget and NavigationSidebarNativeDropZone for Improved Clarity and Structure
- Removed unused import statements and consolidated drop target properties directly within the component.
- Simplified the drop target ID generation logic for better readability.
- Updated the drop target attributes to use data attributes directly in the JSX, enhancing clarity.

These changes contribute to a more organized and efficient implementation of the NavigationItemDropTarget and NavigationSidebarNativeDropZone components.
2026-02-06 14:27:53 +05:30
Abdul Rahman 485e880dab Refactor CommandMenuNewSidebarItemPage for Improved Clarity and Structure
- Reorganized import statements for better code clarity and consistency.
- Simplified the rendering logic by consolidating the object menu item rendering into a dedicated function.
- Updated filtering logic to enhance readability and maintainability.

These changes contribute to a more efficient and organized implementation of the CommandMenuNewSidebarItemPage component.
2026-02-06 14:19:24 +05:30
Abdul Rahman 1a047fec3d Refactor CommandMenuNavigationMenuItemEditPage for Improved Clarity and Structure
- Removed redundant imports to streamline the component.
- Updated conditional rendering logic to enhance readability and maintainability.
- Consolidated rendering logic for object and view items into a single function, improving code organization.

These changes contribute to a more efficient and organized implementation of the CommandMenuNavigationMenuItemEditPage component.
2026-02-06 14:16:59 +05:30
Abdul Rahman 4c4d47b5e6 Refactor NavigationMenuEditModeBar for Improved Clarity and Structure
- Added the `useRecoilValue` import to manage state more effectively.
- Updated the icon variable name from `PaintIcon` to `IconPaint` for better clarity.
- Reintroduced conditional rendering for the navigation menu edit mode bar to enhance readability.

These changes contribute to a more organized and maintainable implementation of the NavigationMenuEditModeBar component.
2026-02-06 08:47:04 +05:30
Abdul Rahman 6dd068a8d0 Refactor NavigationItemDropTarget for Improved Structure and Clarity
- Moved the import statement for `ReactNode`, `useContext`, and `useRef` to enhance code organization.
- Adjusted the import order to maintain consistency with other components.
- These changes contribute to a more efficient and organized implementation of the NavigationItemDropTarget component.
2026-02-06 08:44:12 +05:30
Abdul Rahman 8858696e0d Refactor Navigation Drawer Components for Improved Structure and Clarity
- Removed the `NavigationDrawerItemForLink` component and integrated its functionality directly into the `NavigationDrawerSectionForWorkspaceItems` component, enhancing code clarity and reducing complexity.
- Updated the rendering logic to utilize the `NavigationDrawerItem` component, streamlining the handling of link items and improving maintainability.
- Enhanced icon color handling by incorporating the `getNavigationMenuItemIconColors` utility, ensuring consistent styling across navigation items.

These changes contribute to a more efficient and organized implementation of the navigation drawer components.
2026-02-06 08:43:11 +05:30
Abdul Rahman abf32ad5ee Refactor AddToNavigationDragPreview for Improved Clarity and Consistency
- Introduced the `isDefined` utility function to enhance the clarity of icon rendering logic.
- Simplified padding assignments in the styled component for better readability.
- Streamlined the conditional rendering of icons to improve the component structure.

These changes contribute to a more organized and efficient implementation of the AddToNavigationDragPreview component.
2026-02-06 08:41:23 +05:30
Abdul Rahman 61c1e850a9 Refactor AddToNavigationDragHandle for Improved Readability and Consistency
- Introduced utility function `isDefined` to enhance the clarity of icon rendering logic.
- Simplified icon size and stroke assignments by storing them in variables, improving code maintainability.
- Updated conditional rendering for icons to streamline the component structure.

These changes contribute to a more organized and efficient implementation of the AddToNavigationDragHandle component.
2026-02-06 08:39:09 +05:30
Abdul Rahman 1bf80a3ca4 Refactor CommandMenu Components to Enhance Rendering and Simplify Logic
- Replaced the `CommandMenuSelectObjectForViewMenuItem` component with a more streamlined implementation using `SelectableListItem` and `CommandMenuItem`, improving rendering efficiency and clarity.
- Integrated the `useIcons` hook to manage icon retrieval, enhancing code maintainability.
- Updated the `CommandMenuNewSidebarItemViewObjectPickerSubView` and `CommandMenuNewSidebarItemViewSystemSubView` components to utilize the new rendering approach, ensuring consistency across the command menu.

These changes contribute to a more efficient and organized implementation of the command menu components.
2026-02-06 08:36:33 +05:30
Abdul Rahman dd0b8de3cc Refactor CommandMenuObjectMenuItem for Improved Click Handling and Clarity
- Simplified the click handling logic by removing unnecessary checks and directly returning if `defaultViewId` is not defined.
- Enhanced the rendering logic for `CommandMenuItem` components by consolidating the payload structure and ensuring the `disabled` prop is set correctly.

These changes contribute to a more efficient and maintainable implementation of the command menu components.
2026-02-06 08:34:24 +05:30
Abdul Rahman 1882fdf871 Refactor CommandMenuNewSidebarItemViewSystemSubView to Use Custom Hook for Filtering
- Integrated the `useFilteredPickerItems` custom hook to streamline filtering logic and enhance code clarity.
- Simplified the handling of no results text and selectable item IDs, improving overall component structure.

These changes contribute to a more efficient and maintainable implementation of the command menu components.
2026-02-06 08:32:43 +05:30
Abdul Rahman 09bf32df7b Refactor CommandMenuNewSidebarItemViewPickerSubView to Use Custom Hook for Filtering
- Replaced manual filtering logic with the `useFilteredPickerItems` custom hook, enhancing code clarity and maintainability.
- Streamlined the handling of no results text and improved the rendering of selectable list items.

These changes contribute to a more efficient and organized implementation of the command menu components.
2026-02-06 08:31:29 +05:30
Abdul Rahman a97eafc067 Refactor CommandMenuNewSidebarItemViewObjectPickerSubView to Utilize Custom Hook for Filtering
- Replaced the manual filtering logic with the `useFilteredPickerItems` custom hook, enhancing code clarity and maintainability.
- Streamlined the handling of selectable item IDs and no results text, improving the overall structure of the component.

These changes contribute to a more efficient and organized implementation of the command menu components.
2026-02-06 08:29:41 +05:30
Abdul Rahman 923bb1e75d Refactor CommandMenuNewSidebarItemRecordSubView for Improved Permission Handling
- Simplified the logic for filtering non-readable object metadata items by consolidating the permission check into a single line, enhancing code clarity and maintainability.
- Removed redundant variable assignments to streamline the component structure.

These changes contribute to a more efficient and organized implementation of the command menu components.
2026-02-06 08:28:05 +05:30
Abdul Rahman 8a2b6f8976 Enhance CommandMenuNavigationMenuItemEditPage with Utility Functions and Improved Rendering
- Introduced the `includeCurrentObjectIfMissing` utility function to streamline the inclusion of the current object in object pickers, enhancing code clarity and maintainability.
- Refactored the rendering logic for object menu items to utilize a dedicated `renderObjectMenuItem` function, improving readability and reducing redundancy.
- Updated sorting logic for object arrays to ensure consistent ordering based on `labelPlural`.

These changes contribute to a more efficient and organized implementation of the command menu components.
2026-02-06 08:26:07 +05:30
Abdul Rahman 04c29958d5 Refactor CommandMenuEditLinkItemView to Enhance URL Handling
- Introduced the `getAbsoluteUrl` utility function to streamline URL normalization when updating links.
- Simplified the logic for setting the link URL by directly using `selectedItem.link`, improving code clarity and maintainability.
- Removed redundant variables to enhance the overall structure of the component.

These changes contribute to a more efficient and readable implementation of the command menu components.
2026-02-06 08:21:41 +05:30
Abdul Rahman 8912fb6588 Update CommandMenuEditOwnerSection to Simplify Disabled Prop Usage
- Changed the `disabled` prop in `CommandMenuEditOwnerSection` to a shorthand boolean syntax for improved clarity.
- Removed the redundant `true` value assignment, enhancing code readability and maintainability.

These changes contribute to a cleaner implementation of the command menu components.
2026-02-06 08:19:31 +05:30
Abdul Rahman 8672bbc13a Refactor CommandMenuEdit Components for Improved Clarity
- Updated `CommandMenuEditLinkItemView` and `CommandMenuEditObjectViewBase` components to simplify prop usage by removing redundant `true` values for boolean props.
- Enhanced code readability and maintainability by streamlining the component structure.

These changes contribute to a cleaner and more efficient command menu implementation.
2026-02-06 08:18:03 +05:30
Abdul Rahman 04e8a67f9a Refactor CommandMenuEditFolderPickerSubView for Improved Folder Handling
- Introduced utility functions `getDescendantFolderIds` and `excludeCurrentFolder` to enhance the logic for managing folder selections.
- Simplified the logic for determining which folders to display based on the current selection and search query, improving code clarity and maintainability.
- Updated the rendering logic to ensure consistent handling of folder options, enhancing the user experience within the command menu.

These changes streamline the folder selection process, improving the overall functionality and readability of the command menu component.
2026-02-06 08:15:57 +05:30
Abdul Rahman 27dd5f2ba5 Remove CommandMenuEditFolderItemView and Update CommandMenuNavigationMenuItemEditPage
- Deleted the `CommandMenuEditFolderItemView` component to streamline the codebase.
- Integrated its functionality directly into `CommandMenuNavigationMenuItemEditPage`, enhancing component cohesion and reducing dependencies.

These changes improve maintainability by consolidating related logic within a single component, simplifying the overall structure of the command menu.
2026-02-06 08:12:25 +05:30
Abdul Rahman a7a55322e1 Remove CommandMenuEditDefaultView and Integrate Its Logic into CommandMenuNavigationMenuItemEditPage
- Deleted the `CommandMenuEditDefaultView` component to streamline the codebase.
- Integrated its functionality directly into `CommandMenuNavigationMenuItemEditPage`, enhancing component cohesion and reducing dependencies.

These changes improve maintainability by consolidating related logic within a single component, simplifying the overall structure of the command menu.
2026-02-06 08:10:53 +05:30
Abdul Rahman 9f44d0a56f Remove CommandMenuSharedStyles and Integrate Styles Directly into CommandMenuNavigationMenuItemEditPage
- Deleted the `CommandMenuSharedStyles` file to streamline the codebase.
- Moved the styled components `StyledCommandMenuPlaceholder` and `StyledCommandMenuPageContainer` directly into `CommandMenuNavigationMenuItemEditPage`, enhancing component encapsulation and reducing dependencies.

These changes improve maintainability by consolidating styles within the relevant component, simplifying the overall structure of the command menu.
2026-02-06 08:08:22 +05:30
Abdul Rahman b7f9112287 Refactor CommandMenuFolderLinkInfo to Use Styled Navigation Menu Item Icon
- Replaced the `CommandMenuNavigationMenuItemIcon` component with a direct implementation of `StyledNavigationMenuItemIconContainer` in `CommandMenuFolderLinkInfo`, enhancing code clarity and reducing component complexity.
- Removed the now-unnecessary `CommandMenuNavigationMenuItemIcon` file to streamline the codebase.

These changes improve the maintainability of the command menu components by simplifying the icon rendering logic.
2026-02-06 08:06:15 +05:30
Abdul Rahman 5462dc5ba7 Add CommandMenuFolderLinkInfo Component and Remove CommandMenuLinkInfo
- Introduced `CommandMenuFolderLinkInfo` to consolidate folder and link item handling within the command menu, improving code organization and reducing redundancy.
- Removed the deprecated `CommandMenuLinkInfo` component to streamline the codebase.
- Updated `CommandMenuPageInfo` to utilize the new `CommandMenuFolderLinkInfo` for rendering, enhancing maintainability and clarity.

These changes enhance the command menu's functionality by simplifying the component structure and improving item management.
2026-02-06 08:04:11 +05:30
Abdul Rahman 73c529ecc6 Refactor CommandMenuLinkInfo for Improved Clarity and Logic
- Renamed `selectedId` to `selectedNavigationMenuItemInEditMode` for better understanding of its purpose.
- Updated the logic for retrieving the selected link to enhance readability and maintainability.
- Ensured consistent return statements for null checks, improving overall code structure.

These changes streamline the handling of navigation items within the command menu, enhancing code clarity and functionality.
2026-02-06 07:57:22 +05:30
Abdul Rahman 3d16040a39 Refactor Command Menu Components for Enhanced Readability and Logic
- Updated `CommandMenuFolderInfo` to improve variable naming, changing `selectedId` to `selectedNavigationMenuItemInEditMode` for clarity.
- Simplified conditional checks in `CommandMenuPageInfo` for rendering folder and link components, enhancing code readability.
- Ensured consistent return statements for null checks, improving overall code structure.

These changes enhance the maintainability and clarity of the command menu components, streamlining the logic for item handling.
2026-02-06 07:56:13 +05:30
Abdul Rahman 02dd826738 Refactor Command Menu Components for Improved Item Handling
- Updated `CommandMenuFolderInfo` and `CommandMenuLinkInfo` to utilize direct IDs for selected items, enhancing clarity and maintainability.
- Simplified the logic for retrieving selected folder and link items by using a unified `selectedId` variable.
- Refactored save functions to improve naming consistency and ensure default values are correctly applied.

These changes streamline the command menu components, improving overall code readability and functionality.
2026-02-06 07:54:35 +05:30
Abdul Rahman ff1f0aafe3 Refactor Navigation Menu Item Handling to Use New Utility Functions
- Replaced the deprecated `useFlattenedWorkspaceSectionItemsForLookup` hook with `useWorkspaceSectionItems` across multiple components, including `CommandMenuFolderInfo`, `CommandMenuLinkInfo`, and `CommandMenuPageInfo`, to streamline item retrieval.
- Introduced `getNavigationMenuItemType` utility to enhance clarity in determining item types, improving the readability of conditional checks.
- Updated related components and hooks to ensure consistency with the new item handling approach, enhancing maintainability and code structure.

These changes simplify the navigation menu item management, improving overall code clarity and functionality.
2026-02-06 07:45:34 +05:30
Abdul Rahman 0f9882bcd9 Refactor Navigation Menu Item Handling to Use Direct IDs
- Removed the `getWorkspaceSectionItemId` utility function and updated components to directly access item IDs, simplifying the logic for identifying navigation menu items.
- Adjusted various components, including `CommandMenuFolderInfo`, `CommandMenuLinkInfo`, and `CommandMenuNavigationMenuItemEditPage`, to utilize the new ID structure, enhancing code clarity and maintainability.
- Updated related hooks and types to reflect the changes in item ID handling, ensuring consistency across the navigation menu item management.

These changes streamline the navigation item handling process, improving the overall structure and readability of the codebase.
2026-02-05 18:56:41 +05:30
Abdul Rahman 20dbfcfc31 Refactor CommandMenuPageInfo to Improve Navigation Item Handling
- Renamed `selectedFolder` to `selectedNavItem` for clarity in distinguishing between folder and link types.
- Updated conditional checks to use the new `selectedNavItem` variable, enhancing readability and maintainability of the component's logic.

These changes streamline the handling of navigation items within the command menu, improving code clarity and functionality.
2026-02-05 18:35:31 +05:30
Abdul Rahman df86efed33 Enhance Command Menu Components with Application ID Integration
- Added `applicationId` prop to `CommandMenuEditFolderItemView`, `CommandMenuEditLinkItemView`, and `CommandMenuEditOwnerSection` for improved context handling.
- Updated rendering logic in `CommandMenuNavigationMenuItemEditPage` to pass the correct `applicationId` to folder items.
- Removed unused selectable item IDs in `CommandMenuEditFolderItemView` and `CommandMenuEditLinkItemView` for cleaner code.

These changes improve the modularity and functionality of command menu components, enhancing the user experience by providing relevant application context.
2026-02-05 18:23:24 +05:30
Abdul Rahman 439ac3ffc6 Add options to useCommandMenuHotKeys for form tag handling
- Introduced `enableOnFormTags` option in the `useCommandMenuHotKeys` hook to control hotkey activation within form elements, enhancing flexibility in user interactions.
- Updated the options for focused element hotkeys to include the same `enableOnFormTags` setting, ensuring consistent behavior across different contexts.

These changes improve the usability of hotkeys in the command menu, allowing for better integration with form elements.
2026-02-05 16:59:25 +05:30
Abdul Rahman e9cd87c284 Add useFilteredPickerItems Hook and Refactor Command Menu Components
- Introduced the `useFilteredPickerItems` hook to streamline the filtering of items based on a search query, enhancing reusability across command menu components.
- Deleted several outdated components, including `CommandMenuEditObjectPickerSubView`, `CommandMenuEditObjectPickerSystemSubView`, and `CommandMenuEditViewItemView`, to simplify the codebase.
- Added `CommandMenuEditObjectViewBase`, `CommandMenuObjectPickerSubView`, and `CommandMenuSystemObjectPickerSubView` to improve the organization and functionality of object selection within the command menu.
- Updated `CommandMenuNavigationMenuItemEditPage` and `CommandMenuNewSidebarItemPage` to utilize the new picker components, enhancing the user experience and maintaining consistency.

These changes improve the structure and maintainability of the command menu, providing a more efficient interface for object selection and management.
2026-02-05 16:54:43 +05:30
Abdul Rahman d04df4b52a Refactor useNavigationMenuItemMoveRemove Hook to Improve Position Swapping Logic
- Introduced a new helper function, `swapPositionsInDraft`, to streamline the logic for swapping positions of navigation menu items in the draft state.
- Replaced inline position swapping logic with the new helper function for better readability and maintainability.

These changes enhance the clarity of the `useNavigationMenuItemMoveRemove` hook, making it easier to manage item positions within the navigation menu.
2026-02-05 16:40:51 +05:30
Abdul Rahman 5b6798f9fc Refactor Command Menu Sub View to Integrate Navigation Header
- Replaced the custom back button with the `SidePanelSubPageNavigationHeader` component for improved navigation consistency.
- Removed unused styled components related to the back button, streamlining the code and enhancing maintainability.

These changes enhance the user experience by providing a more cohesive navigation interface within the command menu.
2026-02-05 16:27:54 +05:30
Abdul Rahman 9c6a117d8a Refactor useMouseDownNavigation Hook to Simplify Event Handling
- Removed the `preventDefault` option from the `useMouseDownNavigation` hook to streamline the API and ensure consistent behavior.
- Updated event handling to always prevent default actions for regular clicks, enhancing navigation reliability.

These changes improve the clarity and usability of the `useMouseDownNavigation` hook, ensuring a more predictable user experience.
2026-02-05 15:13:10 +05:30
Abdul Rahman 1061f81285 Enhance Command Menu Folder Picker and Organize Actions Components
- Updated `CommandMenuEditFolderPickerSubView` to include `currentFolderId` for improved folder selection logic, ensuring that the current folder is excluded from the options when applicable.
- Replaced `IconFolder` with `IconFolderPlus` in both `CommandMenuEditFolderPickerSubView` and `CommandMenuEditOrganizeActions` for a more intuitive icon representation.
- Refactored folder filtering logic to enhance clarity and maintainability, improving the user experience when selecting folders.

These changes streamline the folder selection process and enhance the visual consistency of the command menu components.
2026-02-05 14:44:12 +05:30
Abdul Rahman 45af0ca1a0 Refactor Command Menu Components to Integrate Owner Section
- Replaced hardcoded owner items in `CommandMenuEditFolderItemView` and `CommandMenuEditLinkItemView` with a new `CommandMenuEditOwnerSection` component for better modularity and reusability.
- Cleaned up imports and removed unused code to enhance readability and maintainability across multiple command menu components.
- Streamlined the rendering logic in `CommandMenuEditObjectPickerSubView` and `CommandMenuEditViewPickerSubView` for improved clarity.

These changes improve the structure of the command menu components, making them more maintainable and enhancing the overall user experience.
2026-02-05 14:40:34 +05:30
Abdul Rahman 5153c73a6e Refactor Command Menu Components to Use Flattened Workspace Section Items
- Replaced instances of `useWorkspaceSectionItems` with `useFlattenedWorkspaceSectionItemsForLookup` in multiple command menu components, including `CommandMenuFolderInfo`, `CommandMenuLinkInfo`, and `CommandMenuPageInfo`, to streamline data retrieval.
- Enhanced drag-and-drop functionality in `CommandMenuItemWithAddToNavigationDrag` by introducing a new constant for folder drag types and updating event handling.
- Added `NavigationDropTargetContext` to manage drag-and-drop states, improving the user experience when interacting with navigation items.
- Introduced `NavigationItemDropTarget` component to facilitate drop target behavior for navigation items, enhancing the overall drag-and-drop interface.

These changes improve code maintainability and enhance the user experience by providing a more efficient and intuitive command menu interface.
2026-02-05 14:34:07 +05:30
Abdul Rahman 71e4db6bf6 Fix import order and clean up GraphQL type definitions in generated metadata
- Moved the Apollo import statement to the top of the file for consistency.
- Removed duplicate type definition for `NavigationMenuItemFieldsFragment`, ensuring clarity and reducing redundancy.
- Ensured proper formatting and consistency in type definitions throughout the file.

These changes enhance code readability and maintainability in the generated GraphQL metadata.
2026-02-05 11:59:25 +05:30
Abdul Rahman 8237a41b20 Merge branch 'main' into feat/navbar-customization 2026-02-05 11:58:44 +05:30
Abdul Rahman ee57a19866 Remove CommandMenuEditFolderRenameSubView Component
- Deleted the `CommandMenuEditFolderRenameSubView` component from the command menu, streamlining the editing interface.
- Updated `CommandMenuNavigationMenuItemEditPage` to remove references to the deleted component, enhancing code clarity and maintainability.

These changes simplify the command menu structure and improve overall code organization.
2026-02-05 10:53:21 +05:30
Abdul Rahman d4d06a835a Refactor Command Menu Components for Improved Icon Handling and Styling
- Updated `CommandMenuNavigationMenuItemIcon` to utilize `StyledNavigationMenuItemIconContainer` for consistent icon styling based on theme colors.
- Introduced `CommandMenuSharedStyles` for shared styles across command menu components, enhancing maintainability and reducing redundancy.
- Refactored `CommandMenuNavigationMenuItemEditPage` and other components to leverage new shared styles, improving code clarity and user interface consistency.

These changes enhance the user experience by providing a more cohesive and visually appealing command menu interface.
2026-02-05 10:50:55 +05:30
Abdul Rahman 6b2bf17bec Add Command Menu Components for Enhanced Editing Functionality
- Introduced several new components including `CommandMenuEditDefaultView`, `CommandMenuEditFolderItemView`, `CommandMenuEditFolderPickerSubView`, `CommandMenuEditFolderRenameSubView`, `CommandMenuEditLinkItemView`, `CommandMenuEditObjectItemView`, `CommandMenuEditObjectPickerSubView`, `CommandMenuEditObjectPickerSystemSubView`, `CommandMenuEditViewItemView`, and `CommandMenuEditViewPickerSubView` to improve the editing capabilities within the command menu.
- Refactored `CommandMenuNavigationMenuItemEditPage` to utilize these new components, enhancing the organization and functionality of the editing interface.
- Streamlined the search functionality and improved user feedback through better handling of selectable items and no results scenarios.

These changes significantly enhance the user experience by providing a more intuitive and flexible interface for editing navigation items within the command menu.
2026-02-05 10:47:27 +05:30
Abdul Rahman 8f079b2768 Refactor CommandMenuNavigationMenuItemEditPage for improved search handling
- Updated the `CommandMenuNavigationMenuItemEditPage` to enhance the search functionality by introducing clearer handling of empty search results.
- Refactored the logic for generating selectable item IDs to improve clarity and maintainability.
- Added conditional rendering for no results text, providing better user feedback when no system objects are found.

These changes enhance the user experience by making the search interface more intuitive and responsive to user input.
2026-02-05 10:31:00 +05:30
Abdul Rahman 479dabc16f Implement New Sidebar Item Components for Command Menu
- Added `CommandMenuNewSidebarItemMainMenu` and `CommandMenuNewSidebarItemRecordSubView` components to enhance the command menu's sidebar item functionality.
- Refactored `CommandMenuNewSidebarItemPage` to utilize the new components, streamlining the process of adding new items and managing records.
- Optimized existing logic by removing unnecessary hooks and simplifying state management, improving code clarity and maintainability.

These changes significantly enhance the user experience by providing a more intuitive interface for adding and managing sidebar items within the command menu.
2026-02-05 10:26:18 +05:30
Abdul Rahman 3682e544b1 Add Command Menu Components for Enhanced Navigation Functionality
- Introduced several new components including `CommandMenuEditOrganizeActions`, `CommandMenuObjectMenuItem`, `CommandMenuSelectObjectForEditMenuItem`, `CommandMenuSelectObjectForViewMenuItem`, and hooks for managing navigation menu object metadata from drafts.
- Refactored `CommandMenuNavigationMenuItemEditPage` and `CommandMenuNewSidebarItemPage` to utilize these new components, improving the organization and functionality of the command menu.
- Removed redundant code and optimized existing logic for better maintainability and clarity.

These changes significantly enhance the user experience by providing a more intuitive and flexible interface for managing navigation items within the command menu.
2026-02-05 10:17:00 +05:30
Abdul Rahman ea920aa7e4 Enhance CommandMenuSubViewWithSearch component and integrate into navigation menu item edit page
- Updated `CommandMenuSubViewWithSearch` to accept optional `searchInputProps` and made `children` prop optional for improved flexibility.
- Refactored `CommandMenuNavigationMenuItemEditPage` to utilize `CommandMenuSubViewWithSearch`, streamlining the search functionality and enhancing the user interface.
- Removed redundant styled components from `CommandMenuNavigationMenuItemEditPage`, improving code clarity and maintainability.

These changes improve the overall user experience by providing a more consistent and flexible search interface within the command menu.
2026-02-05 09:21:18 +05:30
Abdul Rahman e1925c5451 Refactor command menu components for improved icon handling and search functionality
- Introduced `CommandMenuNavigationMenuItemIcon` component to standardize icon rendering for folder and link items in the command menu.
- Updated `CommandMenuFolderInfo` and `CommandMenuLinkInfo` to utilize the new icon component, enhancing code reusability and maintainability.
- Added `CommandMenuSubViewWithSearch` component to streamline search functionality within the command menu, replacing custom search implementations in `CommandMenuNewSidebarItemPage`.
- Refactored utility functions for workspace section item ID retrieval, improving clarity and reducing redundancy.

These changes enhance the user experience by providing a consistent interface for icons and improving search capabilities within the command menu.
2026-02-05 09:15:27 +05:30
Abdul Rahman aeb62f46a4 Add CommandMenuItemWithAddToNavigationDrag component for enhanced drag-and-drop functionality
- Introduced `CommandMenuItemWithAddToNavigationDrag` component to facilitate drag-and-drop interactions within the command menu.
- Updated `CommandMenuNewSidebarItemPage` to utilize the new component, streamlining the addition of navigation items.
- Refactored existing drag-and-drop logic into the new component, improving code organization and maintainability.
- Enhanced visual feedback during drag operations by integrating drag preview functionality.

These changes significantly improve the user experience by making it easier to add items to the navigation menu through intuitive drag-and-drop actions.
2026-02-05 09:09:04 +05:30
Abdul Rahman ebef2204b5 Enhance navigation menu item functionality with drag-and-drop support
- Introduced new components for drag-and-drop functionality within the navigation menu, including `AddToNavigationDragHandle`, `NavigationItemDropTarget`, and `NavigationSidebarNativeDropZone`.
- Updated existing components to support drag-and-drop interactions, allowing users to easily rearrange navigation items and add new items through drag events.
- Enhanced the `useAddToNavigationMenuDraft` hook to manage the addition of items at specific positions based on drag-and-drop actions.
- Improved the user experience by providing visual feedback during drag operations and ensuring seamless integration with existing navigation menu functionalities.

These changes significantly enhance the usability of the navigation menu, making it more intuitive for users to manage their navigation items.
2026-02-05 08:50:00 +05:30
Abdul Rahman b9e5dd3ab9 Add CommandMenuLinkInfo component and integrate into CommandMenuPageInfo
- Introduced `CommandMenuLinkInfo` component to manage link-specific interactions within the command menu.
- Updated `CommandMenuPageInfo` to conditionally render `CommandMenuLinkInfo` when editing a navigation menu item of type link.
- Enhanced navigation menu item editing capabilities by allowing users to manage links directly within the command menu.

These changes improve the user experience by providing a dedicated interface for link management, streamlining navigation and editing processes.
2026-02-05 07:36:44 +05:30
Abdul Rahman b26b28f7e0 Add link field to NavigationMenuItem and related inputs
- Introduced a new optional `link` field in the `NavigationMenuItem` type, allowing for external links to be associated with navigation items.
- Updated the `CreateNavigationMenuItemInput`, `UpdateNavigationMenuItemInput`, and related DTOs to include the `link` field.
- Modified the database schema with a migration to add the `link` column to the `navigationMenuItem` table.
- Enhanced validation logic to accommodate the new `link` field, ensuring proper handling during navigation menu item creation and updates.

These changes improve the flexibility of navigation menu items by enabling the inclusion of external links, enhancing user navigation capabilities.
2026-02-05 07:27:24 +05:30
Abdul Rahman fc592d3e7d Add CommandMenuFolderInfo component and integrate into CommandMenuPageInfo
- Introduced `CommandMenuFolderInfo` component to manage folder-specific interactions within the command menu.
- Updated `CommandMenuPageInfo` to conditionally render `CommandMenuFolderInfo` when editing a navigation menu item in folder mode.
- Enhanced folder management capabilities by adding folder creation and editing functionalities in the command menu.

These changes improve the user experience by providing a dedicated interface for folder management within the command menu, streamlining navigation and editing processes.
2026-02-05 01:08:55 +05:30
Abdul Rahman 41aa60da2e Integrate command menu functionality into NavigationMenuEditModeBar
- Added `useCommandMenu` hook to manage command menu interactions.
- Updated save draft logic to close the command menu upon successful save, enhancing user experience during navigation menu edits.

This change improves the responsiveness of the navigation menu editing process by ensuring the command menu closes automatically after saving changes.
2026-02-04 23:52:24 +05:30
Abdul Rahman 5110c27c13 Enhance NavigationDrawerItem to include secondary label for object metadata items
- Added a `secondaryLabel` prop to `NavigationDrawerItemForObjectMetadataItem` component.
- The `secondaryLabel` is conditionally set based on whether the item is a record or a view with a custom name, improving the clarity of displayed metadata.

This change enhances the user interface by providing additional context for object metadata items in the navigation drawer.
2026-02-04 23:51:06 +05:30
Abdul Rahman 2cf2ea6ff8 Merge branch 'main' into feat/navbar-customization 2026-02-04 23:34:10 +05:30
Abdul Rahman c516f95374 Add Object Selection for View Editing in Command Menu
- Introduced `CommandMenuSelectObjectForViewEditMenuItem` component to facilitate object selection for view editing.
- Enhanced state management in `CommandMenuNavigationMenuItemEditPage` to track selected object metadata for view editing.
- Updated logic to filter and sort objects based on their association with views, improving the user experience during object selection.
- Adjusted rendering logic to differentiate between object and view editing modes, ensuring clarity in user interactions.

These changes enhance the command menu's functionality, providing users with a more intuitive experience when managing object views.
2026-02-04 20:25:43 +05:30
Abdul Rahman b32d04bc79 Refactor Workspace Navigation Menu Item Rendering and Sorting Logic
- Updated the `WorkspaceNavigationMenuItemsFolder` component to conditionally render secondary labels based on the view key, improving clarity in navigation item representation.
- Simplified the logic in `sortNavigationMenuItems` to ensure consistent handling of object names, enhancing the accuracy of displayed labels.

These changes enhance the user experience by providing clearer navigation item details and improving the overall sorting logic.
2026-02-04 18:58:58 +05:30
Abdul Rahman 70a338ba91 Enhance Command Menu Navigation Item Editing and Sorting Logic
- Updated the `CommandMenuNavigationMenuItemEditPage` to handle empty folder states more gracefully, displaying a custom message when no folders are available.
- Refactored the logic for generating selectable item IDs to include a fallback for empty states.
- Improved the sorting logic in `sortNavigationMenuItems` to correctly handle index views and associated metadata, ensuring accurate display of labels and icons.
- Adjusted test cases to reflect changes in object naming conventions and ensure consistency in expected outcomes.

These changes improve the user experience by providing clearer feedback and more accurate representations of navigation items in the command menu.
2026-02-04 18:51:36 +05:30
Abdul Rahman e2d4375bc7 Refactor Navigation Menu Item Draft Management and Enhance Command Menu Functionality
- Consolidated multiple hooks for adding items to the navigation menu draft into a single `useAddToNavigationMenuDraft` hook, streamlining the process for adding objects, views, and records.
- Removed outdated hooks `useAddObjectToNavigationMenuDraft`, `useAddViewToNavigationMenuDraft`, and refactored related components to utilize the new consolidated hook.
- Introduced `useUpdateNavigationMenuItemsDraft` hook to manage updates to navigation menu items in draft state, enhancing item editing capabilities.
- Enhanced the `CommandMenuNavigationMenuItemEditPage` with improved state management and additional functionality for object selection and navigation item manipulation.
- Updated the `useNavigationMenuItemMoveRemove` hook to include a new `moveToFolder` function, allowing for better organization of navigation items.

These changes improve the overall efficiency and usability of the command menu, providing users with a more cohesive experience when managing navigation items.
2026-02-04 18:10:53 +05:30
Abdul Rahman ff92bde46b Enhance Back Navigation Logic in Command Menu New Sidebar Item Page
- Updated the back navigation logic in the `CommandMenuNewSidebarItemPage` to handle system object selections more effectively.
- Introduced a check for system object metadata to set the appropriate navigation option when returning to the view object list.
- Improved user experience by ensuring the correct state is maintained during navigation actions.

These changes enhance the functionality of the command menu, providing users with a more intuitive navigation experience when dealing with system objects.
2026-02-04 16:43:48 +05:30
Abdul Rahman c89eee4fdc Enhance Command Menu List with Custom No Results Text
- Added a new `noResultsText` prop to the `CommandMenuList` component, allowing for customizable no results messages.
- Updated the `CommandMenuNewSidebarItemPage` to utilize the new prop, providing context-specific messages based on user input.
- Refactored the logic for displaying results to improve clarity and user experience when no views are found.

These changes enhance the flexibility of the command menu, improving user feedback during navigation item searches.
2026-02-04 16:40:54 +05:30
Abdul Rahman f3f8a0cd02 Add View Support to Command Menu New Sidebar Item Page
- Integrated functionality for adding views to the navigation menu draft within the `CommandMenuNewSidebarItemPage`.
- Introduced a new hook, `useAddViewToNavigationMenuDraft`, to manage view additions effectively.
- Updated state management to accommodate view selection and search inputs, enhancing user interaction.
- Refactored related components and utilities to support view handling, improving overall command menu functionality.

These changes enhance the command menu's capabilities, allowing users to manage views alongside other navigation items seamlessly.
2026-02-04 16:38:47 +05:30
Abdul Rahman 9aaefbb765 Enhance Command Menu New Sidebar Item Page with System Object Support
- Added support for system objects in the `CommandMenuNewSidebarItemPage`, allowing users to filter and select system-related items.
- Introduced new state management for system object search input and updated the back navigation logic for improved user experience.
- Refactored object metadata filtering to include system objects, enhancing the overall functionality of the command menu.
- Improved search functionality by implementing a dedicated search input for objects, streamlining the selection process.

These changes enhance the command menu's capabilities, providing users with more comprehensive options for managing navigation items.
2026-02-04 14:47:50 +05:30
Abdul Rahman df50bf41e6 Remove unused targetObjectMetadataId assignment in useSaveNavigationMenuItemsDraft hook
- Eliminated the assignment of targetObjectMetadataId to undefined when viewId is defined, streamlining the input handling logic.
- This change improves the clarity and efficiency of the hook's functionality, ensuring only relevant data is processed.
2026-02-04 14:46:26 +05:30
Abdul Rahman 38659223c7 Add New Sidebar Item Page to Command Menu and Update Navigation Structure
- Introduced the `CommandMenuNewSidebarItemPage` component to facilitate the addition of new items to the navigation menu.
- Updated the `CommandMenuPagesConfig` to include the new sidebar item page, enhancing navigation options.
- Implemented hooks for adding objects and records to the navigation menu draft, improving item management.
- Refactored the `NavigationMenuEditModeBar` to support saving drafts and handling loading states, streamlining the editing process.

These changes enhance the command menu's functionality, providing users with more options for managing navigation items effectively.
2026-02-03 20:20:53 +05:30
Abdul Rahman 4611e83719 Enhance Command Menu Navigation Item Editing with Move and Remove Functionality
- Introduced the ability to move navigation menu items up and down within the command menu, improving item organization.
- Added a remove option to delete selected navigation menu items, streamlining item management.
- Refactored the `CommandMenuNavigationMenuItemEditPage` component to utilize new hooks for item manipulation and state management.
- Updated related components and hooks to ensure consistent handling of navigation menu items.

These changes enhance the user experience by providing more control over navigation menu item arrangements and management.
2026-02-03 13:17:44 +05:30
Abdul Rahman a65322dce0 Update NavigationDrawerItem Width Calculation for Enhanced Layout
- Adjusted the width calculation in the `NavigationDrawerItem` component to account for additional spacing when right options are present.
- Improved the responsiveness of the navigation drawer by refining the width logic for both expanded and collapsed states.

These changes enhance the visual consistency and usability of the navigation drawer items.
2026-02-03 13:07:13 +05:30
Abdul Rahman b38039f687 Refactor Navigation Menu Item State Management and Update Component Logic
- Replaced the `selectedWorkspaceObjectMetadataItemIdInEditModeState` with `selectedNavigationMenuItemInEditModeState` to streamline state management for navigation menu items.
- Updated components to utilize the new state, enhancing clarity and consistency across the navigation menu.
- Refactored hooks and component logic to improve handling of navigation menu item selection and editing, ensuring a more intuitive user experience.
- Introduced new utility functions and styled components to support the updated navigation structure.

These changes enhance the overall functionality and maintainability of the navigation menu system.
2026-02-03 08:32:06 +05:30
Abdul Rahman 9f9c766013 Refactor Navigation Menu Item Structure and Introduce Workspace Section Items
- Updated the navigation menu item components to utilize user-specific navigation items, enhancing the organization of workspace-related views.
- Introduced a new `WorkspaceNavigationMenuItemsFolder` component to manage folder items within the workspace navigation.
- Refactored hooks to separate workspace and user navigation items, improving data handling and clarity.
- Added utility functions to identify navigation menu item folders, streamlining the integration of folder items in the navigation structure.

These changes enhance the user experience by providing a more structured and intuitive navigation menu for workspace items.
2026-02-03 08:06:27 +05:30
Abdul Rahman 9e487f0a44 Add Workflows Folder and Update Navigation Menu Structure
- Introduced a new 'workflowsFolder' item in the standard navigation menu, enhancing organization of workflow-related views.
- Added new entries for 'workflowsFolderAllWorkflows', 'workflowsFolderAllWorkflowRuns', and 'workflowsFolderAllWorkflowVersions' to improve navigation and access to workflow data.
- Created utility functions for generating flat metadata for folder items, streamlining the integration of new navigation items.
- Updated existing view and view field utilities to include support for workflow versions, ensuring comprehensive coverage in the navigation structure.

These changes enhance the user experience by providing a clearer and more structured navigation menu for workflows and their associated views.
2026-02-03 07:35:16 +05:30
Abdul Rahman 4224522d98 Merge branch 'main' into feat/navbar-customization 2026-02-03 01:16:06 +05:30
Abdul Rahman 2d049774d9 Refactor NavigationDrawerItemForObjectMetadataItem to Simplify Component Structure
- Removed unused imports and state management related to views and context store, streamlining the component.
- Simplified the rendering logic by eliminating the collapsible container and directly rendering the `NavigationDrawerItem`.
- This refactor enhances readability and maintainability of the code while preserving existing functionality.
2026-02-03 00:48:00 +05:30
Abdul Rahman e80574b837 Enhance Workspace Navigation Menu Items with Active Item Click Handling
- Added support for handling active item clicks in the `WorkspaceNavigationMenuItems` component, allowing users to interact with items even when not in edit mode.
- Introduced a new styled container for right icons to improve layout and spacing.
- Updated `NavigationDrawerItemForObjectMetadataItem` and `NavigationDrawerSectionForObjectMetadataItems` components to accommodate the new click handling logic, enhancing user experience and interaction consistency.

These changes improve the functionality and usability of the navigation menu, making it more intuitive for users.
2026-02-03 00:40:40 +05:30
Abdul Rahman 3672bb7700 Add Navigation Menu Item Edit Page and Update Command Menu Configuration
- Introduced `CommandMenuNavigationMenuItemEditPage` for editing navigation menu items.
- Updated `COMMAND_MENU_PAGES_CONFIG` to include the new edit page.
- Added state management for selected navigation menu item in edit mode.
- Enhanced `WorkspaceNavigationMenuItems` to support opening the edit page and handling edit mode interactions.

These changes improve the user experience by allowing direct editing of navigation menu items within the command menu.
2026-02-02 20:14:48 +05:30
Abdul Rahman c8a7ce4ff6 Add Navigation Menu Edit Mode Bar and Enhance Workspace Navigation Items 2026-02-02 17:15:32 +05:30
Abdul Rahman 2affeaff7e Enhance Save and Cancel Buttons with Inverted Style Support
- Added an `inverted` prop to `CancelButton`, allowing for a tertiary button style.
- Updated `SaveButton` to support an `inverted` prop, changing its appearance based on the prop value.
- Modified `SaveAndCancelButtons` to pass the `inverted` prop to both buttons, ensuring consistent styling.

These changes improve the visual flexibility of the buttons in the settings module, enhancing user experience.
2026-02-02 17:09:49 +05:30
Abdul Rahman 986a36a8fd Add navigation menu edit mode hooks and state management
- Introduced `useNavigationMenuEditModeActions` for managing edit mode actions, including entering and canceling edit mode.
- Added `useNavigationMenuItemsDraftState` to handle draft state of navigation menu items, determining workspace items based on edit mode.
- Created `isNavigationMenuInEditModeState` and `navigationMenuItemsDraftState` atoms for managing edit mode status and draft items.

These additions enhance the functionality for editing navigation menu items, improving user experience and customization options.
2026-02-02 14:45:15 +05:30
Abdul Rahman ef617aac54 Add IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED feature flag
- Introduced the IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED flag across the GraphQL schema and server-side enums.
- Updated the workspace entity manager tests to include the new feature flag.
- Enhanced the seed feature flags utility to support the new flag.

These changes enable editing capabilities for navigation menu items, improving customization options.
2026-02-02 14:40:21 +05:30
Abdul Rahman 2a5c2cf62d Merge branch 'main' into feat/navbar-customization 2026-02-02 14:23:38 +05:30
Abdul Rahman 8cbbc1b6bf Refactor NavigationMenuItemIcon to enhance icon color handling
- Added `IconColor` to the `useGetStandardObjectIcon` hook for improved icon color customization.
- Simplified logic for determining icon background color based on the presence of `targetRecordId` and `viewId`.
- Consolidated avatar rendering logic to reduce redundancy and improve readability.

These changes enhance the visual consistency and flexibility of navigation menu items.
2026-02-02 07:56:13 +05:30
Abdul Rahman ad10e0a076 Enhance navigation menu item components with icon color customization
- Introduced `getNavigationMenuItemIconColors` utility to manage icon colors based on the theme.
- Updated `CurrentWorkspaceMemberNavigationMenuItems` and `NavigationDrawerItemForObjectMetadataItem` to utilize the new icon color utility.
- Refactored `NavigationMenuItemIcon` to include a styled background for icons, improving visual consistency.
- Adjusted `NavigationDrawerItem` and `NavigationDrawerSubItem` to accept and apply background colors for icons.

These changes improve the visual representation of navigation items and ensure consistent theming across the application.
2026-02-02 07:33:35 +05:30
176 changed files with 10696 additions and 669 deletions
+3 -3
View File
@@ -62,9 +62,9 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 50,
lines: 48.9,
functions: 40.9,
statements: 49.5,
lines: 48,
functions: 40,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
@@ -714,6 +714,13 @@ export enum CommandMenuItemAvailabilityType {
SINGLE_RECORD = 'SINGLE_RECORD'
}
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
/** Workflow version ID */
workflowVersionId?: InputMaybe<Scalars['UUID']>;
};
export enum ConfigSource {
DATABASE = 'DATABASE',
DEFAULT = 'DEFAULT',
@@ -960,6 +967,13 @@ export type CreateDefaultLogicFunctionInput = {
toolInputSchema?: InputMaybe<Scalars['JSON']>;
};
export type CreateDraftFromWorkflowVersionInput = {
/** Workflow ID */
workflowId: Scalars['UUID'];
/** Workflow version ID */
workflowVersionIdToCopy: Scalars['UUID'];
};
export type CreateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
@@ -994,6 +1008,8 @@ export type CreateFrontComponentInput = {
export type CreateNavigationMenuItemInput = {
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Int']>;
targetObjectMetadataId?: InputMaybe<Scalars['UUID']>;
@@ -1151,6 +1167,36 @@ export type CreateWebhookInput = {
targetUrl: Scalars['String'];
};
export type CreateWorkflowVersionEdgeInput = {
/** Workflow version source step ID */
source: Scalars['String'];
/** Workflow version source step connection options */
sourceConnectionOptions?: InputMaybe<Scalars['JSON']>;
/** Workflow version target step ID */
target: Scalars['String'];
/** Workflow version ID */
workflowVersionId: Scalars['String'];
};
export type CreateWorkflowVersionStepInput = {
/** Default settings for the step */
defaultSettings?: InputMaybe<Scalars['JSON']>;
/** Step ID */
id?: InputMaybe<Scalars['String']>;
/** Next step ID */
nextStepId?: InputMaybe<Scalars['UUID']>;
/** Parent step connection options */
parentStepConnectionOptions?: InputMaybe<Scalars['JSON']>;
/** Parent step ID */
parentStepId?: InputMaybe<Scalars['String']>;
/** Step position */
position?: InputMaybe<WorkflowStepPositionInput>;
/** New step type */
stepType: Scalars['String'];
/** Workflow version ID */
workflowVersionId: Scalars['UUID'];
};
export type CursorPaging = {
/** Paginate after opaque cursor */
after?: InputMaybe<Scalars['ConnectionCursor']>;
@@ -1172,6 +1218,17 @@ export enum DatabaseEventAction {
UPSERTED = 'UPSERTED'
}
export type DateTimeFilter = {
eq?: InputMaybe<Scalars['DateTime']>;
gt?: InputMaybe<Scalars['DateTime']>;
gte?: InputMaybe<Scalars['DateTime']>;
in?: InputMaybe<Array<Scalars['DateTime']>>;
is?: InputMaybe<FilterIs>;
lt?: InputMaybe<Scalars['DateTime']>;
lte?: InputMaybe<Scalars['DateTime']>;
neq?: InputMaybe<Scalars['DateTime']>;
};
export type DeleteApprovedAccessDomainInput = {
id: Scalars['UUID'];
};
@@ -1222,6 +1279,13 @@ export type DeleteViewGroupInput = {
id: Scalars['UUID'];
};
export type DeleteWorkflowVersionStepInput = {
/** Step to delete ID */
stepId: Scalars['String'];
/** Workflow version ID */
workflowVersionId: Scalars['UUID'];
};
export type DeletedWorkspaceMember = {
__typename?: 'DeletedWorkspaceMember';
avatarUrl?: Maybe<Scalars['String']>;
@@ -1262,6 +1326,18 @@ export type DomainValidRecords = {
records: Array<DomainRecord>;
};
export type DuplicateWorkflowInput = {
/** Workflow ID to duplicate */
workflowIdToDuplicate: Scalars['UUID'];
/** Workflow version ID to copy */
workflowVersionIdToCopy: Scalars['UUID'];
};
export type DuplicateWorkflowVersionStepInput = {
stepId: Scalars['String'];
workflowVersionId: Scalars['String'];
};
export type DuplicatedDashboard = {
__typename?: 'DuplicatedDashboard';
createdAt: Scalars['String'];
@@ -1422,6 +1498,7 @@ export enum FeatureFlagKey {
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED',
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED',
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
@@ -1576,6 +1653,11 @@ export type FilesConfiguration = {
configurationType: WidgetConfigurationType;
};
export enum FilterIs {
NotNull = 'NotNull',
Null = 'Null'
}
export type FindAvailableSsoidpOutput = {
__typename?: 'FindAvailableSSOIDPOutput';
id: Scalars['UUID'];
@@ -2010,6 +2092,7 @@ export enum ModelProvider {
export type Mutation = {
__typename?: 'Mutation';
activateSkill: Skill;
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace;
addQueryToEventStream: Scalars['Boolean'];
assignRoleToAgent: Scalars['Boolean'];
@@ -2021,6 +2104,7 @@ export type Mutation = {
checkCustomDomainValidRecords?: Maybe<DomainValidRecords>;
checkPublicDomainValidRecords?: Maybe<DomainValidRecords>;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON'];
createApiKey: ApiKey;
createApprovedAccessDomain: ApprovedAccessDomain;
createChatThread: AgentChatThread;
@@ -2033,6 +2117,7 @@ export type Mutation = {
createCoreViewSort: CoreViewSort;
createDatabaseConfigVariable: Scalars['Boolean'];
createDefaultLogicFunction: LogicFunction;
createDraftFromWorkflowVersion: WorkflowVersionDto;
createEmailingDomain: EmailingDomain;
createFile: File;
createFrontComponent: FrontComponent;
@@ -2054,7 +2139,10 @@ export type Mutation = {
createSAMLIdentityProvider: SetupSsoOutput;
createSkill: Skill;
createWebhook: Webhook;
createWorkflowVersionEdge: WorkflowVersionStepChanges;
createWorkflowVersionStep: WorkflowVersionStepChanges;
deactivateSkill: Skill;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCommandMenuItem: CommandMenuItem;
deleteCoreView: Scalars['Boolean'];
@@ -2082,6 +2170,8 @@ export type Mutation = {
deleteUser: User;
deleteUserFromWorkspace: UserWorkspace;
deleteWebhook: Webhook;
deleteWorkflowVersionEdge: WorkflowVersionStepChanges;
deleteWorkflowVersionStep: WorkflowVersionStepChanges;
deleteWorkspaceInvitation: Scalars['String'];
destroyCoreView: Scalars['Boolean'];
destroyCoreViewField: CoreViewField;
@@ -2093,7 +2183,10 @@ export type Mutation = {
destroyPageLayoutTab: Scalars['Boolean'];
destroyPageLayoutWidget: Scalars['Boolean'];
disablePostgresProxy: PostgresCredentials;
dismissReconnectAccountBanner: Scalars['Boolean'];
duplicateDashboard: DuplicatedDashboard;
duplicateWorkflow: WorkflowVersionDto;
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
editSSOIdentityProvider: EditSsoOutput;
emailPasswordResetLink: EmailPasswordResetLinkOutput;
enablePostgresProxy: PostgresCredentials;
@@ -2119,6 +2212,7 @@ export type Mutation = {
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runEvaluationInput: AgentTurn;
runWorkflowVersion: RunWorkflowVersionOutput;
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
sendInvitations: SendInvitationsOutput;
setMeteredSubscriptionPrice: BillingUpdateOutput;
@@ -2129,9 +2223,12 @@ export type Mutation = {
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
startChannelSync: ChannelSyncSuccess;
stopWorkflowRun: WorkflowRun;
submitFormStep: Scalars['Boolean'];
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
syncApplication: Scalars['Boolean'];
testHttpRequest: TestHttpRequestOutput;
trackAnalytics: Analytics;
uninstallApplication: Scalars['Boolean'];
updateApiKey?: Maybe<ApiKey>;
@@ -2160,6 +2257,9 @@ export type Mutation = {
updateSkill: Skill;
updateUserEmail: Scalars['Boolean'];
updateWebhook: Webhook;
updateWorkflowRunStep: WorkflowAction;
updateWorkflowVersionPositions: Scalars['Boolean'];
updateWorkflowVersionStep: WorkflowAction;
updateWorkspace: Workspace;
updateWorkspaceFeatureFlag: Scalars['Boolean'];
updateWorkspaceMemberRole: WorkspaceMember;
@@ -2188,6 +2288,11 @@ export type MutationActivateSkillArgs = {
};
export type MutationActivateWorkflowVersionArgs = {
workflowVersionId: Scalars['UUID'];
};
export type MutationActivateWorkspaceArgs = {
data: ActivateWorkspaceInput;
};
@@ -2230,6 +2335,11 @@ export type MutationCheckoutSessionArgs = {
};
export type MutationComputeStepOutputSchemaArgs = {
input: ComputeStepOutputSchemaInput;
};
export type MutationCreateApiKeyArgs = {
input: CreateApiKeyInput;
};
@@ -2286,6 +2396,11 @@ export type MutationCreateDefaultLogicFunctionArgs = {
};
export type MutationCreateDraftFromWorkflowVersionArgs = {
input: CreateDraftFromWorkflowVersionInput;
};
export type MutationCreateEmailingDomainArgs = {
domain: Scalars['String'];
driver: EmailingDomainDriver;
@@ -2395,11 +2510,26 @@ export type MutationCreateWebhookArgs = {
};
export type MutationCreateWorkflowVersionEdgeArgs = {
input: CreateWorkflowVersionEdgeInput;
};
export type MutationCreateWorkflowVersionStepArgs = {
input: CreateWorkflowVersionStepInput;
};
export type MutationDeactivateSkillArgs = {
id: Scalars['UUID'];
};
export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['UUID'];
};
export type MutationDeleteApprovedAccessDomainArgs = {
input: DeleteApprovedAccessDomainInput;
};
@@ -2526,6 +2656,16 @@ export type MutationDeleteWebhookArgs = {
};
export type MutationDeleteWorkflowVersionEdgeArgs = {
input: CreateWorkflowVersionEdgeInput;
};
export type MutationDeleteWorkflowVersionStepArgs = {
input: DeleteWorkflowVersionStepInput;
};
export type MutationDeleteWorkspaceInvitationArgs = {
appTokenId: Scalars['String'];
};
@@ -2576,11 +2716,26 @@ export type MutationDestroyPageLayoutWidgetArgs = {
};
export type MutationDismissReconnectAccountBannerArgs = {
connectedAccountId: Scalars['UUID'];
};
export type MutationDuplicateDashboardArgs = {
id: Scalars['UUID'];
};
export type MutationDuplicateWorkflowArgs = {
input: DuplicateWorkflowInput;
};
export type MutationDuplicateWorkflowVersionStepArgs = {
input: DuplicateWorkflowVersionStepInput;
};
export type MutationEditSsoIdentityProviderArgs = {
input: EditSsoInput;
};
@@ -2697,6 +2852,11 @@ export type MutationRunEvaluationInputArgs = {
};
export type MutationRunWorkflowVersionArgs = {
input: RunWorkflowVersionInput;
};
export type MutationSaveImapSmtpCaldavAccountArgs = {
accountOwnerId: Scalars['UUID'];
connectionParameters: EmailAccountConnectionParameters;
@@ -2750,11 +2910,26 @@ export type MutationStartChannelSyncArgs = {
};
export type MutationStopWorkflowRunArgs = {
workflowRunId: Scalars['UUID'];
};
export type MutationSubmitFormStepArgs = {
input: SubmitFormStepInput;
};
export type MutationSyncApplicationArgs = {
manifest: Scalars['JSON'];
};
export type MutationTestHttpRequestArgs = {
input: TestHttpRequestInput;
};
export type MutationTrackAnalyticsArgs = {
event?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
@@ -2910,6 +3085,21 @@ export type MutationUpdateWebhookArgs = {
};
export type MutationUpdateWorkflowRunStepArgs = {
input: UpdateWorkflowRunStepInput;
};
export type MutationUpdateWorkflowVersionPositionsArgs = {
input: UpdateWorkflowVersionPositionsInput;
};
export type MutationUpdateWorkflowVersionStepArgs = {
input: UpdateWorkflowVersionStepInput;
};
export type MutationUpdateWorkspaceArgs = {
data: UpdateWorkspaceInput;
};
@@ -3029,7 +3219,9 @@ export type NavigationMenuItem = {
applicationId?: Maybe<Scalars['UUID']>;
createdAt: Scalars['DateTime'];
folderId?: Maybe<Scalars['UUID']>;
icon?: Maybe<Scalars['String']>;
id: Scalars['UUID'];
link?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
position: Scalars['Float'];
targetObjectMetadataId?: Maybe<Scalars['UUID']>;
@@ -3169,6 +3361,16 @@ export type ObjectRecordEventProperties = {
updatedFields?: Maybe<Array<Scalars['String']>>;
};
export type ObjectRecordFilterInput = {
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
createdAt?: InputMaybe<DateTimeFilter>;
deletedAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<UuidFilter>;
not?: InputMaybe<ObjectRecordFilterInput>;
or?: InputMaybe<Array<ObjectRecordFilterInput>>;
updatedAt?: InputMaybe<DateTimeFilter>;
};
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
export enum ObjectRecordGroupByDateGranularity {
DAY = 'DAY',
@@ -3506,6 +3708,12 @@ export type Query = {
getRoles: Array<Role>;
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
getSystemHealthStatus: SystemHealth;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromOpportunityId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromOpportunityId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
getToolIndex: Array<ToolIndexEntry>;
index: Index;
indexMetadatas: IndexConnection;
@@ -3516,6 +3724,7 @@ export type Query = {
object: Object;
objects: ObjectConnection;
pieChartData: PieChartDataOutput;
search: SearchResultConnection;
skill?: Maybe<Skill>;
skills: Array<Skill>;
validatePasswordResetToken: ValidatePasswordResetTokenOutput;
@@ -3768,6 +3977,48 @@ export type QueryGetQueueMetricsArgs = {
};
export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
companyId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineCalendarEventsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
personId: Scalars['UUID'];
};
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineThreadsFromOpportunityIdArgs = {
opportunityId: Scalars['UUID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
export type QueryGetTimelineThreadsFromPersonIdArgs = {
page: Scalars['Int'];
pageSize: Scalars['Int'];
personId: Scalars['UUID'];
};
export type QueryIndexArgs = {
id: Scalars['UUID'];
};
@@ -3805,6 +4056,16 @@ export type QueryPieChartDataArgs = {
};
export type QuerySearchArgs = {
after?: InputMaybe<Scalars['String']>;
excludedObjectNameSingulars?: InputMaybe<Array<Scalars['String']>>;
filter?: InputMaybe<ObjectRecordFilterInput>;
includedObjectNameSingulars?: InputMaybe<Array<Scalars['String']>>;
limit: Scalars['Int'];
searchInput: Scalars['String'];
};
export type QuerySkillArgs = {
id: Scalars['UUID'];
};
@@ -4031,6 +4292,20 @@ export enum RowLevelPermissionPredicateOperand {
VECTOR_SEARCH = 'VECTOR_SEARCH'
}
export type RunWorkflowVersionInput = {
/** Execution result in JSON format */
payload?: InputMaybe<Scalars['JSON']>;
/** Workflow run ID */
workflowRunId?: InputMaybe<Scalars['UUID']>;
/** Workflow version ID */
workflowVersionId: Scalars['UUID'];
};
export type RunWorkflowVersionOutput = {
__typename?: 'RunWorkflowVersionOutput';
workflowRunId: Scalars['UUID'];
};
export type SsoConnection = {
__typename?: 'SSOConnection';
id: Scalars['UUID'];
@@ -4066,6 +4341,12 @@ export type SearchRecord = {
tsRankCD: Scalars['Float'];
};
export type SearchResultConnection = {
__typename?: 'SearchResultConnection';
edges: Array<SearchResultEdge>;
pageInfo: SearchResultPageInfo;
};
export type SearchResultEdge = {
__typename?: 'SearchResultEdge';
cursor: Scalars['String'];
@@ -4159,6 +4440,15 @@ export type StandardOverrides = {
translations?: Maybe<Scalars['JSON']>;
};
export type SubmitFormStepInput = {
/** Form response in JSON format */
response: Scalars['JSON'];
/** Workflow step ID */
stepId: Scalars['UUID'];
/** Workflow run ID */
workflowRunId: Scalars['UUID'];
};
export type Subscription = {
__typename?: 'Subscription';
logicFunctionLogs: LogicFunctionLogs;
@@ -4225,6 +4515,35 @@ export type TasksConfiguration = {
configurationType: WidgetConfigurationType;
};
export type TestHttpRequestInput = {
/** Request body */
body?: InputMaybe<Scalars['JSON']>;
/** HTTP headers */
headers?: InputMaybe<Scalars['JSON']>;
/** HTTP method */
method: Scalars['String'];
/** URL to make the request to */
url: Scalars['String'];
};
export type TestHttpRequestOutput = {
__typename?: 'TestHttpRequestOutput';
/** Error information */
error?: Maybe<Scalars['JSON']>;
/** Response headers */
headers?: Maybe<Scalars['JSON']>;
/** Message describing the result */
message: Scalars['String'];
/** Response data */
result?: Maybe<Scalars['JSON']>;
/** HTTP status code */
status?: Maybe<Scalars['Float']>;
/** HTTP status text */
statusText?: Maybe<Scalars['String']>;
/** Whether the request was successful */
success: Scalars['Boolean'];
};
export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent';
conferenceLink: LinksMetadata;
@@ -4252,6 +4571,12 @@ export type TimelineCalendarEventParticipant = {
workspaceMemberId?: Maybe<Scalars['UUID']>;
};
export type TimelineCalendarEventsWithTotal = {
__typename?: 'TimelineCalendarEventsWithTotal';
timelineCalendarEvents: Array<TimelineCalendarEvent>;
totalNumberOfCalendarEvents: Scalars['Int'];
};
export type TimelineConfiguration = {
__typename?: 'TimelineConfiguration';
configurationType: WidgetConfigurationType;
@@ -4282,6 +4607,12 @@ export type TimelineThreadParticipant = {
workspaceMemberId?: Maybe<Scalars['UUID']>;
};
export type TimelineThreadsWithTotal = {
__typename?: 'TimelineThreadsWithTotal';
timelineThreads: Array<TimelineThread>;
totalNumberOfThreads: Scalars['Int'];
};
export type ToolIndexEntry = {
__typename?: 'ToolIndexEntry';
category: Scalars['String'];
@@ -4303,6 +4634,17 @@ export type TwoFactorAuthenticationMethodDto = {
twoFactorAuthenticationMethodId: Scalars['UUID'];
};
export type UuidFilter = {
eq?: InputMaybe<Scalars['UUID']>;
gt?: InputMaybe<Scalars['UUID']>;
gte?: InputMaybe<Scalars['UUID']>;
in?: InputMaybe<Array<Scalars['UUID']>>;
is?: InputMaybe<FilterIs>;
lt?: InputMaybe<Scalars['UUID']>;
lte?: InputMaybe<Scalars['UUID']>;
neq?: InputMaybe<Scalars['UUID']>;
};
export type UuidFilterComparison = {
eq?: InputMaybe<Scalars['UUID']>;
gt?: InputMaybe<Scalars['UUID']>;
@@ -4393,6 +4735,8 @@ export type UpdateLogicFunctionSourceInput = {
export type UpdateNavigationMenuItemInput = {
folderId?: InputMaybe<Scalars['UUID']>;
icon?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
position?: InputMaybe<Scalars['Int']>;
};
@@ -4598,6 +4942,27 @@ export type UpdateWebhookInputUpdates = {
targetUrl?: InputMaybe<Scalars['String']>;
};
export type UpdateWorkflowRunStepInput = {
/** Step to update in JSON format */
step: Scalars['JSON'];
/** Workflow run ID */
workflowRunId: Scalars['UUID'];
};
export type UpdateWorkflowVersionPositionsInput = {
/** Workflow version updated positions */
positions: Array<WorkflowStepPositionUpdateInput>;
/** Workflow version ID */
workflowVersionId: Scalars['UUID'];
};
export type UpdateWorkflowVersionStepInput = {
/** Step to update in JSON format */
step: Scalars['JSON'];
/** Workflow version ID */
workflowVersionId: Scalars['UUID'];
};
export type UpdateWorkspaceInput = {
aiAdditionalInstructions?: InputMaybe<Scalars['String']>;
allowImpersonation?: InputMaybe<Scalars['Boolean']>;
@@ -4886,27 +5251,104 @@ export type WorkerQueueMetrics = {
waiting: Scalars['Float'];
};
export type WorkflowAction = {
__typename?: 'WorkflowAction';
id: Scalars['UUID'];
name: Scalars['String'];
nextStepIds?: Maybe<Array<Scalars['UUID']>>;
position?: Maybe<WorkflowStepPosition>;
settings: Scalars['JSON'];
type: WorkflowActionType;
valid: Scalars['Boolean'];
};
export enum WorkflowActionType {
AI_AGENT = 'AI_AGENT',
CODE = 'CODE',
CREATE_RECORD = 'CREATE_RECORD',
DELAY = 'DELAY',
DELETE_RECORD = 'DELETE_RECORD',
EMPTY = 'EMPTY',
FILTER = 'FILTER',
FIND_RECORDS = 'FIND_RECORDS',
FORM = 'FORM',
HTTP_REQUEST = 'HTTP_REQUEST',
IF_ELSE = 'IF_ELSE',
ITERATOR = 'ITERATOR',
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
SEND_EMAIL = 'SEND_EMAIL',
UPDATE_RECORD = 'UPDATE_RECORD',
UPSERT_RECORD = 'UPSERT_RECORD'
}
export type WorkflowConfiguration = {
__typename?: 'WorkflowConfiguration';
configurationType: WidgetConfigurationType;
};
export type WorkflowRun = {
__typename?: 'WorkflowRun';
id: Scalars['UUID'];
status: WorkflowRunStatusEnum;
};
export type WorkflowRunConfiguration = {
__typename?: 'WorkflowRunConfiguration';
configurationType: WidgetConfigurationType;
};
/** Status of the workflow run */
export enum WorkflowRunStatusEnum {
COMPLETED = 'COMPLETED',
ENQUEUED = 'ENQUEUED',
FAILED = 'FAILED',
NOT_STARTED = 'NOT_STARTED',
RUNNING = 'RUNNING',
STOPPED = 'STOPPED',
STOPPING = 'STOPPING'
}
export type WorkflowStepPosition = {
__typename?: 'WorkflowStepPosition';
x: Scalars['Float'];
y: Scalars['Float'];
};
export type WorkflowStepPositionInput = {
x: Scalars['Float'];
y: Scalars['Float'];
};
export type WorkflowStepPositionUpdateInput = {
/** Step or trigger ID */
id: Scalars['String'];
/** Position of the step or trigger */
position: WorkflowStepPositionInput;
};
export type WorkflowVersionConfiguration = {
__typename?: 'WorkflowVersionConfiguration';
configurationType: WidgetConfigurationType;
};
export type WorkflowVersionDto = {
__typename?: 'WorkflowVersionDTO';
createdAt: Scalars['String'];
id: Scalars['UUID'];
name: Scalars['String'];
status: Scalars['String'];
steps?: Maybe<Scalars['JSON']>;
trigger?: Maybe<Scalars['JSON']>;
updatedAt: Scalars['String'];
workflowId: Scalars['UUID'];
};
export type WorkflowVersionStepChanges = {
__typename?: 'WorkflowVersionStepChanges';
stepsDiff?: Maybe<Scalars['JSON']>;
triggerDiff?: Maybe<Scalars['JSON']>;
};
export type Workspace = {
__typename?: 'Workspace';
activationStatus: WorkspaceActivationStatus;
@@ -5616,42 +6058,42 @@ export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never
export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array<string>, aboutDescription: string, providers: Array<string>, websiteUrl?: string | null, termsUrl?: string | null }> };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
export type NavigationMenuItemFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string };
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
export type NavigationMenuItemQueryFieldsFragment = { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null };
export type CreateNavigationMenuItemMutationVariables = Exact<{
input: CreateNavigationMenuItemInput;
}>;
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type CreateNavigationMenuItemMutation = { __typename?: 'Mutation', createNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type DeleteNavigationMenuItemMutationVariables = Exact<{
id: Scalars['UUID'];
}>;
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type DeleteNavigationMenuItemMutation = { __typename?: 'Mutation', deleteNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type UpdateNavigationMenuItemMutationVariables = Exact<{
input: UpdateOneNavigationMenuItemInput;
}>;
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type UpdateNavigationMenuItemMutation = { __typename?: 'Mutation', updateNavigationMenuItem: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string } };
export type FindManyNavigationMenuItemsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
export type FindManyNavigationMenuItemsQuery = { __typename?: 'Query', navigationMenuItems: Array<{ __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null }> };
export type FindOneNavigationMenuItemQueryVariables = Exact<{
id: Scalars['UUID'];
}>;
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
export type FindOneNavigationMenuItemQuery = { __typename?: 'Query', navigationMenuItem?: { __typename?: 'NavigationMenuItem', id: string, userWorkspaceId?: string | null, targetRecordId?: string | null, targetObjectMetadataId?: string | null, viewId?: string | null, folderId?: string | null, name?: string | null, link?: string | null, icon?: string | null, position: number, applicationId?: string | null, createdAt: string, updatedAt: string, targetRecordIdentifier?: { __typename?: 'RecordIdentifier', id: string, labelIdentifier: string, imageIdentifier?: string | null } | null } | null };
export type ObjectMetadataFieldsFragment = { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> };
@@ -7081,6 +7523,8 @@ export const NavigationMenuItemFieldsFragmentDoc = gql`
viewId
folderId
name
link
icon
position
applicationId
createdAt
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
import { Draggable } from '@hello-pangea/dnd';
import { type ReactNode } from 'react';
type CommandMenuAddToNavDraggablePlaceholderProps = {
index: number;
children: ReactNode;
};
export const CommandMenuAddToNavDraggablePlaceholder = ({
index,
children,
}: CommandMenuAddToNavDraggablePlaceholderProps) => (
<Draggable
draggableId={`add-to-nav-placeholder-${index}`}
index={index}
isDragDisabled={true}
>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
>
{children}
</div>
)}
</Draggable>
);
@@ -0,0 +1,25 @@
import { Droppable, type DroppableProvided } from '@hello-pangea/dnd';
import { type ReactNode, useContext } from 'react';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
type CommandMenuAddToNavDroppableProps = {
children: (provided: DroppableProvided) => ReactNode;
};
export const CommandMenuAddToNavDroppable = ({
children,
}: CommandMenuAddToNavDroppableProps) => {
const { sourceDroppableId } = useContext(NavigationDragSourceContext);
const isDropDisabled = sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
return (
<Droppable
droppableId={ADD_TO_NAV_SOURCE_DROPPABLE_ID}
isDropDisabled={isDropDisabled}
>
{(provided) => children(provided)}
</Droppable>
);
};
@@ -0,0 +1,115 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
const StyledClickableIconWrapper = styled.div`
cursor: pointer;
`;
export const CommandMenuFolderInfo = () => {
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
useRecoilComponentState(
commandMenuShouldFocusTitleInputComponentState,
commandMenuPageInfo.instanceId,
);
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
const { updateFolderInDraft } = useUpdateFolderInDraft();
const defaultLabel = t`New folder`;
const placeholder = t`Folder name`;
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.FOLDER &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
if (!selectedItem) return null;
const itemId = selectedItem.id;
const itemName = selectedItem.name ?? defaultLabel;
const handleChange = (text: string) => {
updateFolderInDraft(itemId, { name: text });
};
const handleSave = () => {
const trimmed = itemName.trim();
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
if (finalName !== itemName) {
updateFolderInDraft(itemId, { name: finalName });
}
};
const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT;
const FolderIconComponent = getIcon(selectedIconKey);
return (
<CommandMenuPageInfoLayout
icon={
<IconPicker
dropdownId="command-menu-folder-icon-picker"
selectedIconKey={selectedIconKey}
onChange={({ iconKey }) =>
updateFolderInDraft(itemId, { icon: iconKey })
}
clickableComponent={
<StyledClickableIconWrapper>
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).folder}
>
<FolderIconComponent
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
</StyledClickableIconWrapper>
}
/>
}
title={
<TitleInput
instanceId={`folder-name-${itemId}`}
sizeVariant="sm"
value={itemName}
onChange={handleChange}
placeholder={placeholder}
onEnter={handleSave}
onEscape={handleSave}
onClickOutside={handleSave}
onTab={handleSave}
onShiftTab={handleSave}
shouldFocus={shouldFocusTitleInput}
onFocus={() => setShouldFocusTitleInput(false)}
/>
}
/>
);
};
@@ -1,11 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { type ReactNode } from 'react';
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { type ReactNode } from 'react';
import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
export type CommandMenuItemProps = {
label: string;
@@ -15,6 +15,7 @@ export type CommandMenuItemProps = {
onClick?: () => void;
Icon?: IconComponent;
hotKeys?: string[];
LeftComponent?: ReactNode;
RightComponent?: ReactNode;
contextualTextPosition?: 'left' | 'right';
hasSubMenu?: boolean;
@@ -31,6 +32,7 @@ export const CommandMenuItem = ({
onClick,
Icon,
hotKeys,
LeftComponent,
RightComponent,
hasSubMenu = false,
isSubMenuOpened = false,
@@ -49,8 +51,9 @@ export const CommandMenuItem = ({
return (
<MenuItem
withIconContainer={true}
LeftIcon={Icon}
withIconContainer={!LeftComponent}
LeftIcon={LeftComponent ? undefined : Icon}
LeftComponent={LeftComponent}
text={label}
contextualText={description}
contextualTextPosition={contextualTextPosition}
@@ -0,0 +1,104 @@
import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode, useState } from 'react';
import { useSetRecoilState } from 'recoil';
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 type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
type CommandMenuItemWithAddToNavigationDragProps = {
icon?: IconComponent;
customIconContent?: ReactNode;
label: string;
description?: string;
id: string;
onClick: () => void;
payload: AddToNavigationDragPayload;
dragIndex?: number;
};
const StyledDraggableMenuItem = styled.div`
cursor: grab;
width: 100%;
&:active {
cursor: grabbing;
}
`;
export const CommandMenuItemWithAddToNavigationDrag = ({
icon,
customIconContent,
label,
description,
id,
onClick,
payload,
dragIndex,
}: CommandMenuItemWithAddToNavigationDragProps) => {
const { t } = useLingui();
const setRegistry = useSetRecoilState(addToNavPayloadRegistryState);
const [isHovered, setIsHovered] = useState(false);
const contextualDescription = isHovered
? t`Drag to add to navbar`
: description;
const DragHandleIcon = () => (
<AddToNavigationDragHandle
icon={icon}
customIconContent={customIconContent}
payload={payload}
isHovered={isHovered}
/>
);
const registerPayload = () => {
if (dragIndex !== undefined) {
setRegistry((prev) => new Map(prev).set(id, payload));
}
};
const menuItemContent = (
<StyledDraggableMenuItem
onMouseEnter={() => {
setIsHovered(true);
registerPayload();
}}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={registerPayload}
>
<CommandMenuItem
Icon={DragHandleIcon}
label={label}
description={contextualDescription}
id={id}
onClick={onClick}
/>
</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>
);
}
return menuItemContent;
};
@@ -0,0 +1,94 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { IconLink } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
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 { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { TitleInput } from '@/ui/input/components/TitleInput';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
export const CommandMenuLinkInfo = () => {
const theme = useTheme();
const { t } = useLingui();
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
useRecoilComponentState(
commandMenuShouldFocusTitleInputComponentState,
commandMenuPageInfo.instanceId,
);
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
const { updateLinkInDraft } = useUpdateLinkInDraft();
const defaultLabel = t`Link label`;
const placeholder = t`Link label`;
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.LINK &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
if (!selectedItem) return null;
const itemId = selectedItem.id;
const itemName = selectedItem.name ?? defaultLabel;
const handleChange = (text: string) => {
updateLinkInDraft(itemId, { name: text });
};
const handleSave = () => {
const trimmed = itemName.trim();
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
if (finalName !== itemName) {
updateLinkInDraft(itemId, { name: finalName });
}
};
return (
<CommandMenuPageInfoLayout
icon={
<StyledNavigationMenuItemIconContainer
$backgroundColor={getNavigationMenuItemIconColors(theme).link}
>
<IconLink
size={theme.spacing(3.5)}
color={theme.grayScale.gray1}
stroke={theme.icon.stroke.md}
/>
</StyledNavigationMenuItemIconContainer>
}
title={
<TitleInput
instanceId={`link-label-${itemId}`}
sizeVariant="sm"
value={itemName}
onChange={handleChange}
placeholder={placeholder}
onEnter={handleSave}
onEscape={handleSave}
onClickOutside={handleSave}
onTab={handleSave}
onShiftTab={handleSave}
shouldFocus={shouldFocusTitleInput}
onFocus={() => setShouldFocusTitleInput(false)}
/>
}
label={t`link`}
/>
);
};
@@ -20,6 +20,7 @@ export type CommandMenuListProps = {
children?: React.ReactNode;
loading?: boolean;
noResults?: boolean;
noResultsText?: string;
};
const StyledInnerList = styled.div`
@@ -60,6 +61,7 @@ export const CommandMenuList = ({
children,
loading = false,
noResults = false,
noResultsText,
}: CommandMenuListProps) => {
const setHasUserSelectedCommand = useSetRecoilState(
hasUserSelectedCommandState,
@@ -91,7 +93,7 @@ export const CommandMenuList = ({
) : null,
)}
{noResults && !loading && (
<StyledEmpty>{t`No results found`}</StyledEmpty>
<StyledEmpty>{noResultsText ?? t`No results found`}</StyledEmpty>
)}
</SelectableList>
</StyledInnerList>
@@ -0,0 +1,46 @@
import { useLingui } from '@lingui/react/macro';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { CommandMenuPageInfoLayout } from '@/command-menu/components/CommandMenuPageInfoLayout';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
export const CommandMenuObjectViewRecordInfo = () => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
const processedItem =
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
? selectedItem
: undefined;
if (!processedItem || !selectedItemLabel) {
return null;
}
const isViewOrRecord = [
NavigationMenuItemType.VIEW,
NavigationMenuItemType.RECORD,
].includes(processedItem.itemType);
if (!isViewOrRecord) {
return null;
}
const label = isViewOrRecord
? processedItem.itemType === NavigationMenuItemType.RECORD
? t`record`
: t`view`
: t`object`;
return (
<CommandMenuPageInfoLayout
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
label={label}
/>
);
};
@@ -1,11 +1,20 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { CommandMenuFolderInfo } from '@/command-menu/components/CommandMenuFolderInfo';
import { CommandMenuLinkInfo } from '@/command-menu/components/CommandMenuLinkInfo';
import { CommandMenuMultipleRecordsInfo } from '@/command-menu/components/CommandMenuMultipleRecordsInfo';
import { CommandMenuObjectViewRecordInfo } from '@/command-menu/components/CommandMenuObjectViewRecordInfo';
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
const StyledPageTitle = styled.div`
@@ -19,10 +28,40 @@ type CommandMenuPageInfoProps = {
};
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
if (!isDefined(pageChip)) {
return null;
}
const isNavigationMenuItemEditPage =
pageChip.page?.page === CommandMenuPages.NavigationMenuItemEdit;
const selectedNavItem = isNavigationMenuItemEditPage
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
if (isNavigationMenuItemEditPage && isDefined(selectedNavItem)) {
const itemType = selectedNavItem.itemType;
if (itemType === NavigationMenuItemType.FOLDER) {
return <CommandMenuFolderInfo />;
}
if (itemType === NavigationMenuItemType.LINK) {
return <CommandMenuLinkInfo />;
}
if (
itemType === NavigationMenuItemType.VIEW ||
itemType === NavigationMenuItemType.RECORD
) {
return <CommandMenuObjectViewRecordInfo />;
}
}
const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
if (isRecordPage && isDefined(pageChip.page?.pageId)) {
@@ -134,6 +134,7 @@ export const CommandMenuSidePanelForDesktop = () => {
isOpen={isCommandMenuOpened}
isResizing={isResizing}
onTransitionEnd={handleTransitionEnd}
data-command-menu-panel=""
>
<StyledSidePanel>
<StyledModalContainer ref={handleModalContainerRef} />
@@ -0,0 +1,87 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
const StyledSubViewContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
`;
const StyledSearchContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
min-width: 0;
padding: ${({ theme }) => theme.spacing(2, 3)};
`;
const StyledSearchInput = styled.input`
background: transparent;
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
padding: 0;
width: 100%;
outline: none;
&::placeholder {
color: ${({ theme }) => theme.font.color.tertiary};
}
`;
const StyledScrollableListWrapper = styled.div`
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
& > * {
flex: 1;
min-height: 0;
}
`;
type CommandMenuSubViewWithSearchProps = {
backBarTitle: string;
onBack: () => void;
searchPlaceholder: string;
searchValue: string;
onSearchChange: (value: string) => void;
searchInputProps?: React.InputHTMLAttributes<HTMLInputElement>;
children?: ReactNode;
};
export const CommandMenuSubViewWithSearch = ({
backBarTitle,
onBack,
searchPlaceholder,
searchValue,
onSearchChange,
searchInputProps,
children,
}: CommandMenuSubViewWithSearchProps) => (
<StyledSubViewContainer>
<SidePanelSubPageNavigationHeader
title={backBarTitle}
onBackClick={onBack}
/>
<StyledSearchContainer>
<StyledSearchInput
placeholder={searchPlaceholder}
value={searchValue}
onChange={(event) => onSearchChange(event.target.value)}
autoFocus
// eslint-disable-next-line react/jsx-props-no-spreading
{...searchInputProps}
/>
</StyledSearchContainer>
{children != null && (
<StyledScrollableListWrapper>{children}</StyledScrollableListWrapper>
)}
</StyledSubViewContainer>
);
@@ -4,6 +4,8 @@ import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/Com
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuFrontComponentPage } from '@/command-menu/pages/front-component/components/CommandMenuFrontComponentPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
import { CommandMenuNavigationMenuItemEditPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNavigationMenuItemEditPage';
import { CommandMenuNewSidebarItemPage } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemPage';
import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings';
import { CommandMenuPageLayoutFieldsLayout } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout';
import { CommandMenuPageLayoutFieldsSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings';
@@ -76,4 +78,9 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
<CommandMenuPageLayoutFieldsLayout />,
],
[CommandMenuPages.ViewFrontComponent, <CommandMenuFrontComponentPage />],
[
CommandMenuPages.NavigationMenuItemEdit,
<CommandMenuNavigationMenuItemEditPage />,
],
[CommandMenuPages.NavigationMenuAddItem, <CommandMenuNewSidebarItemPage />],
]);
@@ -1,12 +1,12 @@
import { useRecoilCallback } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
import { useCloseAnyOpenDropdown } from '@/ui/layout/dropdown/hooks/useCloseAnyOpenDropdown';
import { emitSidePanelOpenEvent } from '@/ui/layout/right-drawer/utils/emitSidePanelOpenEvent';
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
@@ -29,6 +29,7 @@ export const useCommandMenu = () => {
.getValue();
if (isCommandMenuOpened) {
set(addToNavPayloadRegistryState, new Map());
set(isCommandMenuOpenedState, false);
set(isCommandMenuClosingState, true);
closeAnyOpenDropdown();
@@ -86,6 +86,9 @@ export const useCommandMenuHotKeys = () => {
},
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [goBackFromCommandMenu],
options: {
enableOnFormTags: false,
},
});
useHotkeysOnFocusedElement({
@@ -119,6 +122,7 @@ export const useCommandMenuHotKeys = () => {
],
options: {
preventDefault: false,
enableOnFormTags: false,
},
});
};
@@ -0,0 +1,43 @@
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type UseFilteredPickerItemsParams<T> = {
items: T[];
searchQuery: string;
getSearchableValues: (item: T) => string[];
appendSelectableIds?: string[];
};
type UseFilteredPickerItemsResult<T> = {
filteredItems: T[];
selectableItemIds: string[];
isEmpty: boolean;
hasSearchQuery: boolean;
};
export const useFilteredPickerItems = <T extends { id: string }>({
items,
searchQuery,
getSearchableValues,
appendSelectableIds = [],
}: UseFilteredPickerItemsParams<T>): UseFilteredPickerItemsResult<T> => {
const filteredItems = filterBySearchQuery({
items,
searchQuery,
getSearchableValues,
});
const selectableItemIds =
filteredItems.length > 0
? [...filteredItems.map((item) => item.id), ...appendSelectableIds]
: appendSelectableIds;
const isEmpty = filteredItems.length === 0;
const hasSearchQuery = searchQuery.trim().length > 0;
return {
filteredItems,
selectableItemIds,
isEmpty,
hasSearchQuery,
};
};
@@ -0,0 +1,8 @@
export enum CommandMenuNavigationItemActions {
MOVE_UP = 'move-up',
MOVE_DOWN = 'move-down',
MOVE_TO_FOLDER = 'move-to-folder',
ADD_BEFORE = 'add-before',
ADD_AFTER = 'add-after',
REMOVE = 'remove',
}
@@ -0,0 +1,85 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { IconFolderPlus } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFolderPickerSelectionData } from '@/command-menu/pages/navigation-menu-item/hooks/useFolderPickerSelectionData';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { filterBySearchQuery } from '~/utils/filterBySearchQuery';
type CommandMenuEditFolderPickerSubViewProps = {
onBack: () => void;
};
export const CommandMenuEditFolderPickerSubView = ({
onBack,
}: CommandMenuEditFolderPickerSubViewProps) => {
const { t } = useLingui();
const [searchValue, setSearchValue] = useState('');
const { foldersToShow, includeNoFolderOption, handleSelectFolder } =
useFolderPickerSelectionData({ onCloseSubView: onBack });
const filteredFolders = filterBySearchQuery({
items: foldersToShow,
searchQuery: searchValue,
getSearchableValues: (folder) => [folder.name],
});
const isEmpty = filteredFolders.length === 0 && !includeNoFolderOption;
const selectableItemIds = [
...(includeNoFolderOption ? ['no-folder'] : []),
...(filteredFolders.length > 0 ? filteredFolders.map((f) => f.id) : []),
];
const noResultsText =
searchValue.trim().length > 0
? t`No results found`
: t`No folders available`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Move to a folder`}
onBack={onBack}
searchPlaceholder={t`Search a folder...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`Folders`}>
{includeNoFolderOption && (
<SelectableListItem
itemId="no-folder"
onEnter={() => handleSelectFolder(null)}
>
<CommandMenuItem
label={t`No folder`}
id="no-folder"
onClick={() => handleSelectFolder(null)}
/>
</SelectableListItem>
)}
{filteredFolders.map((folder) => (
<SelectableListItem
key={folder.id}
itemId={folder.id}
onEnter={() => handleSelectFolder(folder.id)}
>
<CommandMenuItem
Icon={IconFolderPlus}
label={folder.name}
id={folder.id}
onClick={() => handleSelectFolder(folder.id)}
/>
</SelectableListItem>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,72 @@
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getAbsoluteUrl } from 'twenty-shared/utils';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import {
type OrganizeActionsProps,
CommandMenuEditOrganizeActions,
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { TextInput } from '@/ui/input/components/TextInput';
type CommandMenuEditLinkItemViewProps = OrganizeActionsProps & {
selectedItem: ProcessedNavigationMenuItem;
onUpdateLink: (linkId: string, link: string) => void;
onOpenFolderPicker: () => void;
};
export const CommandMenuEditLinkItemView = ({
selectedItem,
onUpdateLink,
canMoveUp,
canMoveDown,
onOpenFolderPicker,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
}: CommandMenuEditLinkItemViewProps) => {
const { t } = useLingui();
const [urlEditInput, setUrlEditInput] = useState('');
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandGroup heading={t`Customize`}>
<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('');
}
}}
/>
</CommandGroup>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
moveToFolderHasSubMenu
/>
<CommandMenuEditOwnerSection applicationId={selectedItem.applicationId} />
</CommandMenuList>
);
};
@@ -0,0 +1,39 @@
import {
type OrganizeActionsProps,
CommandMenuEditOrganizeActions,
} from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
type CommandMenuEditObjectViewBaseProps = OrganizeActionsProps & {
onOpenFolderPicker: () => void;
};
export const CommandMenuEditObjectViewBase = ({
onOpenFolderPicker,
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
}: CommandMenuEditObjectViewBaseProps) => {
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
return (
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={onOpenFolderPicker}
/>
</CommandMenuList>
);
};
@@ -0,0 +1,125 @@
import { useLingui } from '@lingui/react/macro';
import {
IconChevronDown,
IconChevronUp,
IconFolderSymlink,
IconRowInsertBottom,
IconRowInsertTop,
IconTrash,
} from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuNavigationItemActions } from '@/command-menu/pages/navigation-menu-item/command-menu-navigation-item-actions';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
export type OrganizeActionsProps = {
canMoveUp: boolean;
canMoveDown: boolean;
onMoveUp: () => void;
onMoveDown: () => void;
onRemove: () => void;
onAddBefore?: () => void;
onAddAfter?: () => void;
};
type CommandMenuEditOrganizeActionsProps = OrganizeActionsProps & {
showMoveToFolder?: boolean;
onMoveToFolder?: () => void;
moveToFolderHasSubMenu?: boolean;
};
export const CommandMenuEditOrganizeActions = ({
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
showMoveToFolder = false,
onMoveToFolder,
moveToFolderHasSubMenu = false,
}: CommandMenuEditOrganizeActionsProps) => {
const { t } = useLingui();
return (
<CommandGroup heading={t`Organize`}>
<SelectableListItem
itemId={CommandMenuNavigationItemActions.MOVE_UP}
onEnter={canMoveUp ? onMoveUp : undefined}
>
<CommandMenuItem
Icon={IconChevronUp}
label={t`Move up`}
id={CommandMenuNavigationItemActions.MOVE_UP}
onClick={onMoveUp}
disabled={!canMoveUp}
/>
</SelectableListItem>
<SelectableListItem
itemId={CommandMenuNavigationItemActions.MOVE_DOWN}
onEnter={canMoveDown ? onMoveDown : undefined}
>
<CommandMenuItem
Icon={IconChevronDown}
label={t`Move down`}
id={CommandMenuNavigationItemActions.MOVE_DOWN}
onClick={onMoveDown}
disabled={!canMoveDown}
/>
</SelectableListItem>
{showMoveToFolder && onMoveToFolder && (
<SelectableListItem
itemId={CommandMenuNavigationItemActions.MOVE_TO_FOLDER}
onEnter={onMoveToFolder}
>
<CommandMenuItem
Icon={IconFolderSymlink}
label={t`Move to folder`}
id={CommandMenuNavigationItemActions.MOVE_TO_FOLDER}
hasSubMenu={moveToFolderHasSubMenu}
onClick={onMoveToFolder}
/>
</SelectableListItem>
)}
{onAddBefore && (
<SelectableListItem
itemId={CommandMenuNavigationItemActions.ADD_BEFORE}
onEnter={onAddBefore}
>
<CommandMenuItem
Icon={IconRowInsertTop}
label={t`Add menu item before`}
id={CommandMenuNavigationItemActions.ADD_BEFORE}
onClick={onAddBefore}
/>
</SelectableListItem>
)}
{onAddAfter && (
<SelectableListItem
itemId={CommandMenuNavigationItemActions.ADD_AFTER}
onEnter={onAddAfter}
>
<CommandMenuItem
Icon={IconRowInsertBottom}
label={t`Add menu item after`}
id={CommandMenuNavigationItemActions.ADD_AFTER}
onClick={onAddAfter}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={CommandMenuNavigationItemActions.REMOVE}
onEnter={onRemove}
>
<CommandMenuItem
Icon={IconTrash}
label={t`Remove from sidebar`}
id={CommandMenuNavigationItemActions.REMOVE}
onClick={onRemove}
/>
</SelectableListItem>
</CommandGroup>
);
};
@@ -0,0 +1,54 @@
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconApps } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useFindOneApplicationQuery } from '~/generated-metadata/graphql';
type CommandMenuEditOwnerSectionProps = {
applicationId?: string | null;
};
export const CommandMenuEditOwnerSection = ({
applicationId: applicationIdProp,
}: CommandMenuEditOwnerSectionProps) => {
const { t } = useLingui();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { currentDraft } = useDraftNavigationMenuItems();
const applicationIdFromDraft =
selectedItem && currentDraft
? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId
: undefined;
const applicationId = applicationIdProp ?? applicationIdFromDraft;
const { data } = useFindOneApplicationQuery({
variables: { id: applicationId ?? '' },
skip: !isDefined(applicationId),
});
const applicationName = data?.findOneApplication?.name;
if (!isDefined(applicationName)) {
return null;
}
return (
<CommandGroup heading={t`Owner`}>
<SelectableListItem itemId="owner-app" onEnter={() => {}}>
<CommandMenuItem
Icon={IconApps}
label={applicationName}
id="owner-app"
disabled
/>
</SelectableListItem>
</CommandGroup>
);
};
@@ -0,0 +1,150 @@
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuEditFolderPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditFolderPickerSubView';
import { CommandMenuEditLinkItemView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditLinkItemView';
import { CommandMenuEditObjectViewBase } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditObjectViewBase';
import { CommandMenuEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { CommandMenuEditOwnerSection } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOwnerSection';
import { useNavigationMenuItemEditOrganizeActions } from '@/command-menu/pages/navigation-menu-item/hooks/useNavigationMenuItemEditOrganizeActions';
import { getOrganizeActionsSelectableItemIds } from '@/command-menu/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const StyledCommandMenuPlaceholder = styled.p`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledCommandMenuPageContainer = styled.div`
padding: ${({ theme }) => theme.spacing(3)};
`;
export const CommandMenuNavigationMenuItemEditPage = () => {
const { t } = useLingui();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const selectedItemType = selectedItem?.itemType ?? null;
const [isFolderPickerOpen, setIsFolderPickerOpen] = useState(false);
const setFolderPicker = () => setIsFolderPickerOpen(true);
const {
canMoveUp,
canMoveDown,
onMoveUp,
onMoveDown,
onRemove,
onAddBefore,
onAddAfter,
} = useNavigationMenuItemEditOrganizeActions();
const { updateLinkInDraft } = useUpdateLinkInDraft();
if (!selectedNavigationMenuItemInEditMode || !selectedItemLabel) {
return (
<StyledCommandMenuPageContainer>
<StyledCommandMenuPlaceholder>
{t`Select a navigation item to edit`}
</StyledCommandMenuPlaceholder>
</StyledCommandMenuPageContainer>
);
}
if (isFolderPickerOpen) {
return (
<CommandMenuEditFolderPickerSubView
onBack={() => setIsFolderPickerOpen(false)}
/>
);
}
switch (selectedItemType) {
case NavigationMenuItemType.VIEW:
if (!selectedItemObjectMetadata) return null;
return (
<CommandMenuEditObjectViewBase
onOpenFolderPicker={setFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
case NavigationMenuItemType.LINK:
if (
isDefined(selectedItem) &&
selectedItem.itemType === NavigationMenuItemType.LINK
) {
return (
<CommandMenuEditLinkItemView
key={selectedItem.id}
selectedItem={selectedItem}
onUpdateLink={(linkId, link) => updateLinkInDraft(linkId, { link })}
onOpenFolderPicker={setFolderPicker}
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
);
}
return null;
case NavigationMenuItemType.FOLDER:
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(false)}
>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
/>
<CommandMenuEditOwnerSection />
</CommandMenuList>
);
default:
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
>
<CommandMenuEditOrganizeActions
canMoveUp={canMoveUp}
canMoveDown={canMoveDown}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
onRemove={onRemove}
onAddBefore={onAddBefore}
onAddAfter={onAddAfter}
showMoveToFolder
onMoveToFolder={setFolderPicker}
/>
</CommandMenuList>
);
}
};
@@ -0,0 +1,116 @@
import { useLingui } from '@lingui/react/macro';
import {
IconAddressBook,
IconCube,
IconFolder,
IconLink,
IconList,
} from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { useAddFolderAndLinkToNavigationMenu } from '@/command-menu/pages/navigation-menu-item/hooks/useAddFolderAndLinkToNavigationMenu';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
type CommandMenuNewSidebarItemMainMenuProps = {
onSelectObject: () => void;
onSelectView: () => void;
onSelectRecord: () => void;
};
export const CommandMenuNewSidebarItemMainMenu = ({
onSelectObject,
onSelectView,
onSelectRecord,
}: CommandMenuNewSidebarItemMainMenuProps) => {
const { t } = useLingui();
const { handleAddFolder, handleAddLink } =
useAddFolderAndLinkToNavigationMenu();
return (
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={['object', 'view', 'record', 'folder', 'link']}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Data`}>
<CommandMenuAddToNavDraggablePlaceholder index={0}>
<SelectableListItem itemId="object" onEnter={onSelectObject}>
<CommandMenuItem
Icon={IconCube}
label={t`Object`}
id="object"
hasSubMenu={true}
onClick={onSelectObject}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
<CommandMenuAddToNavDraggablePlaceholder index={1}>
<SelectableListItem itemId="view" onEnter={onSelectView}>
<CommandMenuItem
Icon={IconList}
label={t`View`}
id="view"
hasSubMenu={true}
onClick={onSelectView}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
<CommandMenuAddToNavDraggablePlaceholder index={2}>
<SelectableListItem itemId="record" onEnter={onSelectRecord}>
<CommandMenuItem
Icon={IconAddressBook}
label={t`Record`}
id="record"
hasSubMenu={true}
onClick={onSelectRecord}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
</CommandGroup>
<CommandGroup heading={t`Other`}>
<SelectableListItem itemId="folder" onEnter={handleAddFolder}>
<CommandMenuItemWithAddToNavigationDrag
icon={IconFolder}
label={t`Folder`}
id="folder"
onClick={handleAddFolder}
dragIndex={3}
payload={{
type: NavigationMenuItemType.FOLDER,
folderId: 'new',
name: t`New folder`,
}}
/>
</SelectableListItem>
<SelectableListItem itemId="link" onEnter={handleAddLink}>
<CommandMenuItemWithAddToNavigationDrag
icon={IconLink}
label={t`Link`}
id="link"
onClick={handleAddLink}
dragIndex={4}
payload={{
type: NavigationMenuItemType.LINK,
linkId: 'new',
name: t`Link label`,
link: 'https://www.example.com',
}}
/>
</SelectableListItem>
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
);
};
@@ -0,0 +1,106 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerSubView';
import { CommandMenuSystemObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuSystemObjectPickerSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/command-menu/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ViewKey } from '@/views/types/ViewKey';
type CommandMenuNewSidebarItemObjectFlowProps = {
onBack: () => void;
};
export const CommandMenuNewSidebarItemObjectFlow = ({
onBack,
}: CommandMenuNewSidebarItemObjectFlowProps) => {
const { t } = useLingui();
const { closeCommandMenu } = useCommandMenu();
const [objectSearchInput, setObjectSearchInput] = useState('');
const [systemObjectSearchInput, setSystemObjectSearchInput] = useState('');
const [isInSystemPicker, setIsInSystemPicker] = useState(false);
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter((view) => view.key !== ViewKey.Index)
.map((view) => view.objectMetadataId),
);
const { availableObjectMetadataItems, availableSystemObjectMetadataItems } =
getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleSelectObject = (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => {
addObjectToDraft(
objectMetadataItem.id,
defaultViewId,
currentDraft,
addMenuItemInsertionContext?.targetFolderId,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
const handleBackToObjectList = () => {
setIsInSystemPicker(false);
setSystemObjectSearchInput('');
};
if (isInSystemPicker) {
return (
<CommandMenuSystemObjectPickerSubView
systemObjects={availableSystemObjectMetadataItems}
searchValue={systemObjectSearchInput}
onSearchChange={setSystemObjectSearchInput}
onBack={handleBackToObjectList}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
emptyNoResultsText={t`All system objects are already in the sidebar`}
/>
);
}
return (
<CommandMenuObjectPickerSubView
objects={availableObjectMetadataItems}
searchValue={objectSearchInput}
onSearchChange={setObjectSearchInput}
onBack={onBack}
onOpenSystemPicker={() => setIsInSystemPicker(true)}
isViewItem={false}
onChangeObject={handleSelectObject}
objectMenuItemVariant="add"
/>
);
};
@@ -0,0 +1,35 @@
import { useState } from 'react';
import { CommandMenuNewSidebarItemMainMenu } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemMainMenu';
import { CommandMenuNewSidebarItemObjectFlow } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemObjectFlow';
import { CommandMenuNewSidebarItemRecordSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordSubView';
import { CommandMenuNewSidebarItemViewFlow } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewFlow';
type SelectedOption = 'object' | 'record' | 'view' | null;
export const CommandMenuNewSidebarItemPage = () => {
const [selectedOption, setSelectedOption] = useState<SelectedOption>(null);
const handleBackToMain = () => {
setSelectedOption(null);
};
switch (selectedOption) {
case 'view':
return <CommandMenuNewSidebarItemViewFlow onBack={handleBackToMain} />;
case 'object':
return <CommandMenuNewSidebarItemObjectFlow onBack={handleBackToMain} />;
case 'record':
return (
<CommandMenuNewSidebarItemRecordSubView onBack={handleBackToMain} />
);
default:
return (
<CommandMenuNewSidebarItemMainMenu
onSelectRecord={() => setSelectedOption('record')}
onSelectObject={() => setSelectedOption('object')}
onSelectView={() => setSelectedOption('view')}
/>
);
}
};
@@ -0,0 +1,95 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Avatar } from 'twenty-ui/display';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
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 { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type SearchRecord = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type CommandMenuNewSidebarItemRecordItemProps = {
record: SearchRecord;
dragIndex?: number;
};
export const CommandMenuNewSidebarItemRecordItem = ({
record,
dragIndex,
}: CommandMenuNewSidebarItemRecordItemProps) => {
const { closeCommandMenu } = useCommandMenu();
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
const { currentDraft } = useDraftNavigationMenuItems();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { objectMetadataItems } = useObjectMetadataItems();
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === record.objectNameSingular,
);
const recordPayload: AddToNavigationDragPayload = {
type: NavigationMenuItemType.RECORD,
recordId: record.recordId,
objectMetadataId: objectMetadataItem?.id ?? '',
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
};
const handleSelectRecord = () => {
addRecordToDraft(
{
recordId: record.recordId,
objectNameSingular: record.objectNameSingular,
label: record.label,
imageUrl: record.imageUrl,
},
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
return (
<SelectableListItem itemId={record.recordId} onEnter={handleSelectRecord}>
<CommandMenuItemWithAddToNavigationDrag
customIconContent={
<Avatar
type={
record.objectNameSingular === CoreObjectNameSingular.Company
? 'squared'
: 'rounded'
}
avatarUrl={record.imageUrl}
placeholderColorSeed={record.recordId}
placeholder={record.label}
/>
}
label={record.label}
description={
objectMetadataItem?.labelSingular ?? record.objectNameSingular
}
id={record.recordId}
onClick={handleSelectRecord}
dragIndex={dragIndex}
payload={recordPayload}
/>
</SelectableListItem>
);
};
@@ -0,0 +1,119 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useDebounce } from 'use-debounce';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
import { CommandMenuNewSidebarItemRecordItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemRecordItem';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
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 { useSearchQuery } from '~/generated/graphql';
type SearchRecordBase = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type CommandMenuNewSidebarItemRecordSubViewProps = {
onBack: () => void;
};
export const CommandMenuNewSidebarItemRecordSubView = ({
onBack,
}: CommandMenuNewSidebarItemRecordSubViewProps) => {
const { t } = useLingui();
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const [recordSearchInput, setRecordSearchInput] = useState('');
const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300);
const coreClient = useApolloCoreClient();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const nonReadableObjectMetadataItemsNameSingular = objectMetadataItems
.filter(
(objectMetadataItem) =>
!getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
})?.canReadObjectRecords,
)
.map((objectMetadataItem) => objectMetadataItem.nameSingular);
const { data: searchData, loading: recordSearchLoading } = useSearchQuery({
client: coreClient,
variables: {
searchInput: deferredRecordSearchInput ?? '',
limit: MAX_SEARCH_RESULTS,
excludedObjectNameSingulars: [
'workspaceMember',
...nonReadableObjectMetadataItemsNameSingular,
],
},
});
const workspaceRecordIds = new Set(
currentDraft.flatMap((item) =>
isDefined(item.targetRecordId) ? [item.targetRecordId] : [],
),
);
const searchRecords =
searchData?.search?.edges?.map((edge) => edge.node) ?? [];
const availableSearchRecords = searchRecords.filter(
(record) => !workspaceRecordIds.has(record.recordId),
) as SearchRecordBase[];
const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading;
const selectableItemIds = isEmpty
? []
: availableSearchRecords.map((record) => record.recordId);
const noResultsText =
deferredRecordSearchInput.length > 0
? t`No results found`
: t`Type to search records`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Add a record`}
onBack={onBack}
searchPlaceholder={t`Search records...`}
searchValue={recordSearchInput}
onSearchChange={setRecordSearchInput}
>
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
loading={recordSearchLoading}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Results`}>
{availableSearchRecords.map((record, index) => (
<CommandMenuNewSidebarItemRecordItem
key={record.recordId}
record={record}
dragIndex={index}
/>
))}
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,111 @@
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { CommandMenuNewSidebarItemViewObjectPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewObjectPickerSubView';
import { CommandMenuNewSidebarItemViewPickerSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewPickerSubView';
import { CommandMenuNewSidebarItemViewSystemSubView } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuNewSidebarItemViewSystemSubView';
import { getAvailableObjectMetadataForNewSidebarItem } from '@/command-menu/pages/navigation-menu-item/utils/getAvailableObjectMetadataForNewSidebarItem';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { ViewKey } from '@/views/types/ViewKey';
type CommandMenuNewSidebarItemViewFlowProps = {
onBack: () => void;
};
export const CommandMenuNewSidebarItemViewFlow = ({
onBack,
}: CommandMenuNewSidebarItemViewFlowProps) => {
const [selectedObjectMetadataIdForView, setSelectedObjectMetadataIdForView] =
useState<string | null>(null);
const [objectSearchInput, setObjectSearchInput] = useState('');
const [systemObjectSearchInput, setSystemObjectSearchInput] = useState('');
const [isInSystemPicker, setIsInSystemPicker] = useState(false);
const { currentDraft } = useDraftNavigationMenuItems();
const { objectMetadataItems } = useObjectMetadataItems();
const { activeNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
const { views, objectMetadataIdsWithIndexView } =
useNavigationMenuObjectMetadataFromDraft(currentDraft);
const objectMetadataIdsWithDisplayableViews = new Set(
views
.filter((view) => view.key !== ViewKey.Index)
.map((view) => view.objectMetadataId),
);
const {
objectMetadataItemsWithViews,
availableSystemObjectMetadataItemsForView,
} = getAvailableObjectMetadataForNewSidebarItem({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
});
const handleBackFromViewPicker = () => {
const selectedObjectMetadataItem = isDefined(
selectedObjectMetadataIdForView,
)
? objectMetadataItems.find(
(item) => item.id === selectedObjectMetadataIdForView,
)
: undefined;
const cameFromSystemObjects = selectedObjectMetadataItem?.isSystem ?? false;
setSelectedObjectMetadataIdForView(null);
setIsInSystemPicker(cameFromSystemObjects);
};
const handleBackFromSystemPicker = () => {
setIsInSystemPicker(false);
};
const handleSelectObject = (objectId: string) => {
setSelectedObjectMetadataIdForView(objectId);
};
const handleSelectObjectFromSystem = (objectId: string) => {
setSelectedObjectMetadataIdForView(objectId);
setIsInSystemPicker(false);
};
if (isDefined(selectedObjectMetadataIdForView) && !isInSystemPicker) {
return (
<CommandMenuNewSidebarItemViewPickerSubView
selectedObjectMetadataIdForView={selectedObjectMetadataIdForView}
onBack={handleBackFromViewPicker}
/>
);
}
if (isInSystemPicker) {
return (
<CommandMenuNewSidebarItemViewSystemSubView
systemObjects={availableSystemObjectMetadataItemsForView}
searchValue={systemObjectSearchInput}
onSearchChange={setSystemObjectSearchInput}
onBack={handleBackFromSystemPicker}
onSelectObject={(item) => handleSelectObjectFromSystem(item.id)}
/>
);
}
return (
<CommandMenuNewSidebarItemViewObjectPickerSubView
objects={objectMetadataItemsWithViews}
searchValue={objectSearchInput}
onSearchChange={setObjectSearchInput}
onBack={onBack}
onOpenSystemPicker={() => setIsInSystemPicker(true)}
onSelectObject={(item) => handleSelectObject(item.id)}
showSystemObjectsOption={
availableSystemObjectMetadataItemsForView.length > 0
}
/>
);
};
@@ -0,0 +1,101 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { IconSettings, useIcons } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemViewObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onOpenSystemPicker: () => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
showSystemObjectsOption?: boolean;
};
export const CommandMenuNewSidebarItemViewObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onBack,
onOpenSystemPicker,
onSelectObject,
showSystemObjectsOption = true,
}: CommandMenuNewSidebarItemViewObjectPickerSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: objects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
appendSelectableIds: showSystemObjectsOption ? ['system'] : [],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No objects with views found`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Pick an object`}
onBack={onBack}
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
hasSubMenu
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
{showSystemObjectsOption && (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
)}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,148 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useIcons } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/hooks/useNavigationMenuObjectMetadataFromDraft';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
type CommandMenuNewSidebarItemViewPickerSubViewProps = {
selectedObjectMetadataIdForView: string;
onBack: () => void;
};
export const CommandMenuNewSidebarItemViewPickerSubView = ({
selectedObjectMetadataIdForView,
onBack,
}: CommandMenuNewSidebarItemViewPickerSubViewProps) => {
const { t } = useLingui();
const { getIcon } = useIcons();
const [searchValue, setSearchValue] = useState('');
const { closeCommandMenu } = useCommandMenu();
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
const { currentDraft } = useDraftNavigationMenuItems();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { objectMetadataItems } = useObjectMetadataItems();
const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft);
const viewsForSelectedObject = views
.filter(
(view) =>
view.objectMetadataId === selectedObjectMetadataIdForView &&
view.key !== ViewKey.Index,
)
.sort((a, b) => a.position - b.position);
const selectedObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === selectedObjectMetadataIdForView,
);
const backBarTitle =
selectedObjectMetadataItem?.labelPlural ?? t`Pick a view`;
const {
filteredItems: filteredViews,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useFilteredPickerItems({
items: viewsForSelectedObject,
searchQuery: searchValue,
getSearchableValues: (view) => [view.name],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No custom views available`;
const handleSelectView = (view: View) => {
addViewToDraft(
view.id,
currentDraft,
addMenuItemInsertionContext?.targetFolderId ?? null,
addMenuItemInsertionContext?.targetIndex,
);
setAddMenuItemInsertionContext(null);
closeCommandMenu();
};
return (
<CommandMenuSubViewWithSearch
backBarTitle={backBarTitle}
onBack={onBack}
searchPlaceholder={t`Search a view...`}
searchValue={searchValue}
onSearchChange={setSearchValue}
>
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
<CommandGroup heading={t`Views`}>
{filteredViews.map((view, index) => (
<SelectableListItem
key={view.id}
itemId={view.id}
onEnter={() => handleSelectView(view)}
>
<CommandMenuItemWithAddToNavigationDrag
customIconContent={
selectedObjectMetadataItem ? (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(
selectedObjectMetadataItem.icon,
)}
ViewIcon={getIcon(view.icon)}
/>
) : undefined
}
icon={
selectedObjectMetadataItem
? undefined
: getIcon(view.icon)
}
label={view.name}
id={view.id}
onClick={() => handleSelectView(view)}
dragIndex={index}
payload={{
type: NavigationMenuItemType.VIEW,
viewId: view.id,
label: view.name,
}}
/>
</SelectableListItem>
))}
</CommandGroup>
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,88 @@
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useIcons } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuNewSidebarItemViewSystemSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onSelectObject: (objectMetadataItem: ObjectMetadataItem) => void;
};
export const CommandMenuNewSidebarItemViewSystemSubView = ({
systemObjects,
searchValue,
onSearchChange,
onBack,
onSelectObject,
}: CommandMenuNewSidebarItemViewSystemSubViewProps) => {
const { t } = useLingui();
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const {
filteredItems: filteredSystemObjectMetadataItems,
selectableItemIds,
isEmpty,
hasSearchQuery,
} = useFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: t`No system objects with views found`;
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`System objects`}
onBack={onBack}
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`System objects`}>
{filteredSystemObjectMetadataItems.map((objectMetadataItem) => (
<SelectableListItem
key={objectMetadataItem.id}
itemId={objectMetadataItem.id}
onEnter={() => onSelectObject(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObject(objectMetadataItem)}
/>
</SelectableListItem>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,83 @@
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuItemWithAddToNavigationDrag } from '@/command-menu/components/CommandMenuItemWithAddToNavigationDrag';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
type CommandMenuObjectMenuItemProps = {
objectMetadataItem: ObjectMetadataItem;
onSelect: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
variant: 'add' | 'edit';
dragIndex?: number;
};
export const CommandMenuObjectMenuItem = ({
objectMetadataItem,
onSelect,
variant,
dragIndex,
}: CommandMenuObjectMenuItemProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const defaultViewId = useRecoilValue(
coreIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const Icon = getIcon(objectMetadataItem.icon);
const isDisabled = !isDefined(defaultViewId);
const handleClick = () => {
if (!defaultViewId) {
return;
}
onSelect(objectMetadataItem, defaultViewId);
};
return (
<SelectableListItem itemId={objectMetadataItem.id} onEnter={handleClick}>
{variant === 'add' && !isDisabled ? (
<CommandMenuItemWithAddToNavigationDrag
icon={Icon}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
dragIndex={dragIndex}
payload={{
type: NavigationMenuItemType.OBJECT,
objectMetadataId: objectMetadataItem.id,
defaultViewId: defaultViewId ?? '',
label: objectMetadataItem.labelPlural,
}}
/>
) : (
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={Icon}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={handleClick}
disabled={isDisabled}
/>
)}
</SelectableListItem>
);
};
@@ -0,0 +1,67 @@
import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuObjectMenuItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectMenuItem';
import { IconWithBackground } from '@/navigation-menu-item/components/IconWithBackground';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuObjectPickerItemProps = {
objectMetadataItem: ObjectMetadataItem;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
dragIndex?: number;
};
export const CommandMenuObjectPickerItem = ({
objectMetadataItem,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
dragIndex,
}: CommandMenuObjectPickerItemProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
if (isViewItem && isDefined(onSelectObjectForViewEdit)) {
return (
<SelectableListItem
itemId={objectMetadataItem.id}
onEnter={() => onSelectObjectForViewEdit(objectMetadataItem)}
>
<CommandMenuItem
Icon={({ size, stroke }) => (
<IconWithBackground
Icon={getIcon(objectMetadataItem.icon)}
backgroundColor={iconColors.object}
size={size}
stroke={stroke}
/>
)}
label={objectMetadataItem.labelPlural}
id={objectMetadataItem.id}
onClick={() => onSelectObjectForViewEdit(objectMetadataItem)}
/>
</SelectableListItem>
);
}
return (
<CommandMenuObjectMenuItem
objectMetadataItem={objectMetadataItem}
onSelect={onChangeObject}
variant={objectMenuItemVariant}
dragIndex={dragIndex}
/>
);
};
@@ -0,0 +1,134 @@
import { useLingui } from '@lingui/react/macro';
import { IconSettings } from 'twenty-ui/display';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuAddToNavDraggablePlaceholder } from '@/command-menu/components/CommandMenuAddToNavDraggablePlaceholder';
import { CommandMenuAddToNavDroppable } from '@/command-menu/components/CommandMenuAddToNavDroppable';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
type CommandMenuObjectPickerSubViewProps = {
objects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
onOpenSystemPicker: () => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
};
export const CommandMenuObjectPickerSubView = ({
objects,
searchValue,
onSearchChange,
onBack,
onOpenSystemPicker,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
}: CommandMenuObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: objects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
appendSelectableIds: ['system'],
});
const noResultsText = hasSearchQuery
? t`No results found`
: (emptyNoResultsText ?? t`All objects are already in the sidebar`);
const isAddVariant = objectMenuItemVariant === 'add';
const listContent = (
<CommandGroup heading={t`Objects`}>
{filteredItems.map((objectMetadataItem, index) => (
<CommandMenuObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
dragIndex={isAddVariant ? index : undefined}
/>
))}
{isAddVariant ? (
<CommandMenuAddToNavDraggablePlaceholder index={filteredItems.length}>
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
</CommandMenuAddToNavDraggablePlaceholder>
) : (
<SelectableListItem itemId="system" onEnter={onOpenSystemPicker}>
<CommandMenuItem
Icon={IconSettings}
label={t`System objects`}
id="system"
hasSubMenu={true}
onClick={onOpenSystemPicker}
/>
</SelectableListItem>
)}
</CommandGroup>
);
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`Pick an object`}
onBack={onBack}
searchPlaceholder={t`Search an object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
{isAddVariant ? (
<CommandMenuAddToNavDroppable>
{({ innerRef, droppableProps, placeholder }) => (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div ref={innerRef} {...droppableProps}>
{listContent}
{placeholder}
</div>
</CommandMenuList>
)}
</CommandMenuAddToNavDroppable>
) : (
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
{listContent}
</CommandMenuList>
)}
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,77 @@
import { useLingui } from '@lingui/react/macro';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuObjectPickerItem } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuObjectPickerItem';
import { CommandMenuSubViewWithSearch } from '@/command-menu/components/CommandMenuSubViewWithSearch';
import { useFilteredPickerItems } from '@/command-menu/hooks/useFilteredPickerItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type CommandMenuSystemObjectPickerSubViewProps = {
systemObjects: ObjectMetadataItem[];
searchValue: string;
onSearchChange: (value: string) => void;
onBack: () => void;
isViewItem: boolean;
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
onChangeObject: (
objectMetadataItem: ObjectMetadataItem,
defaultViewId: string,
) => void;
objectMenuItemVariant?: 'add' | 'edit';
emptyNoResultsText?: string;
};
export const CommandMenuSystemObjectPickerSubView = ({
systemObjects,
searchValue,
onSearchChange,
onBack,
isViewItem,
onSelectObjectForViewEdit,
onChangeObject,
objectMenuItemVariant = 'edit',
emptyNoResultsText,
}: CommandMenuSystemObjectPickerSubViewProps) => {
const { t } = useLingui();
const { filteredItems, selectableItemIds, isEmpty, hasSearchQuery } =
useFilteredPickerItems({
items: systemObjects,
searchQuery: searchValue,
getSearchableValues: (item) => [item.labelPlural],
});
const noResultsText = hasSearchQuery
? t`No results found`
: (emptyNoResultsText ?? t`No system objects available`);
return (
<CommandMenuSubViewWithSearch
backBarTitle={t`System objects`}
onBack={onBack}
searchPlaceholder={t`Search a system object...`}
searchValue={searchValue}
onSearchChange={onSearchChange}
>
<CommandMenuList
commandGroups={[]}
selectableItemIds={selectableItemIds}
noResults={isEmpty}
noResultsText={noResultsText}
>
<CommandGroup heading={t`System objects`}>
{filteredItems.map((objectMetadataItem) => (
<CommandMenuObjectPickerItem
key={objectMetadataItem.id}
objectMetadataItem={objectMetadataItem}
isViewItem={isViewItem}
onSelectObjectForViewEdit={onSelectObjectForViewEdit}
onChangeObject={onChangeObject}
objectMenuItemVariant={objectMenuItemVariant}
/>
))}
</CommandGroup>
</CommandMenuList>
</CommandMenuSubViewWithSearch>
);
};
@@ -0,0 +1,71 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink } from 'twenty-ui/display';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
export const useAddFolderAndLinkToNavigationMenu = () => {
const { t } = useLingui();
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
const addMenuItemInsertionContext = useRecoilValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const currentDraft = isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItems;
const addAndOpenEdit = (type: 'folder' | 'link') => {
const targetFolderId = addMenuItemInsertionContext?.targetFolderId ?? null;
const targetIndex = addMenuItemInsertionContext?.targetIndex;
const itemId =
type === 'folder'
? addFolderToDraft(
t`New folder`,
currentDraft,
targetFolderId,
targetIndex,
)
: addLinkToDraft(
t`Link label`,
'www.example.com',
currentDraft,
targetFolderId,
targetIndex,
);
setAddMenuItemInsertionContext(null);
setSelectedNavigationMenuItemInEditMode(itemId);
openNavigationMenuItemInCommandMenu({
pageTitle: type === 'folder' ? t`Edit folder` : t`Edit link`,
pageIcon: type === 'folder' ? IconFolder : IconLink,
focusTitleInput: true,
});
};
return {
handleAddFolder: () => addAndOpenEdit('folder'),
handleAddLink: () => addAndOpenEdit('link'),
};
};
@@ -0,0 +1,107 @@
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsAllFolders';
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
type FolderOption = {
id: string;
name: string;
folderId?: string;
};
const getDescendantFolderIds = (
folderId: string,
allFolders: FolderOption[],
): Set<string> => {
const result = new Set<string>();
for (const folder of allFolders) {
if (folder.folderId !== folderId) continue;
result.add(folder.id);
getDescendantFolderIds(folder.id, allFolders).forEach((id) =>
result.add(id),
);
}
return result;
};
const excludeCurrentFolder = <T extends { id: string }>(
folders: T[],
currentFolderId: string | null,
): T[] =>
!isDefined(currentFolderId)
? folders
: folders.filter((folder) => folder.id !== currentFolderId);
type UseFolderPickerSelectionDataParams = {
onCloseSubView: () => void;
};
export const useFolderPickerSelectionData = ({
onCloseSubView,
}: UseFolderPickerSelectionDataParams) => {
const { closeCommandMenu } = useCommandMenu();
const { moveToFolder } = useNavigationMenuItemMoveRemove();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const selectedItemType = selectedItem?.itemType ?? null;
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();
const selectedFolderId =
selectedItemType === NavigationMenuItemType.FOLDER
? selectedNavigationMenuItemInEditMode
: null;
const currentFolderId =
selectedItemType === NavigationMenuItemType.FOLDER
? (selectedItem?.id ?? null)
: (selectedItem?.folderId ?? null);
const descendantFolderIds =
selectedItemType === NavigationMenuItemType.FOLDER &&
isDefined(selectedFolderId)
? getDescendantFolderIds(selectedFolderId, allFolders)
: new Set<string>();
const includeNoFolderOption =
(selectedItemType === NavigationMenuItemType.FOLDER &&
isDefined(selectedFolderId)) ||
(selectedItemType === NavigationMenuItemType.LINK &&
isDefined(currentFolderId));
const folders =
includeNoFolderOption &&
selectedItemType === NavigationMenuItemType.FOLDER &&
isDefined(selectedFolderId)
? allFolders.filter(
(folder) =>
folder.id !== selectedFolderId &&
!descendantFolderIds.has(folder.id),
)
: excludeCurrentFolder(allFolders, currentFolderId);
const foldersToShow = includeNoFolderOption
? folders
: excludeCurrentFolder(workspaceFolders, currentFolderId);
const handleSelectFolder = (folderId: string | null) => {
if (isDefined(selectedNavigationMenuItemInEditMode)) {
moveToFolder(selectedNavigationMenuItemInEditMode, folderId);
onCloseSubView();
closeCommandMenu();
}
};
return {
foldersToShow,
includeNoFolderOption,
handleSelectFolder,
};
};
@@ -0,0 +1,138 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { type OrganizeActionsProps } from '@/command-menu/pages/navigation-menu-item/components/CommandMenuEditOrganizeActions';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/hooks/useNavigationMenuItemMoveRemove';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
const getAddMenuItemInsertionContext = (
selectedItem: { id: string; folderId?: string | null },
workspaceNavigationMenuItems: Array<{
id: string;
folderId?: string | null;
userWorkspaceId?: string | null;
}>,
offset: 0 | 1,
): AddMenuItemInsertionContext | null => {
const targetFolderId = selectedItem.folderId ?? null;
const itemsInFolder = workspaceNavigationMenuItems.filter(
(item) =>
(item.folderId ?? null) === targetFolderId &&
!isDefined(item.userWorkspaceId),
);
const selectedIndexInFolder = itemsInFolder.findIndex(
(item) => item.id === selectedItem.id,
);
if (selectedIndexInFolder < 0) {
return null;
}
return {
targetFolderId,
targetIndex: selectedIndexInFolder + offset,
};
};
export const useNavigationMenuItemEditOrganizeActions =
(): OrganizeActionsProps => {
const { t } = useLingui();
const { closeCommandMenu } = useCommandMenu();
const { navigateCommandMenu } = useNavigateCommandMenu();
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const setAddMenuItemInsertionContext = useSetRecoilState(
addMenuItemInsertionContextState,
);
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const items = useWorkspaceSectionItems();
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
const folderId = selectedItem?.folderId ?? null;
const siblings = items
.filter(
(item) =>
('folderId' in item ? (item.folderId ?? null) : null) === folderId,
)
.sort((a, b) => a.position - b.position);
const selectedIndexInSiblings = selectedItem
? siblings.findIndex((item) => item.id === selectedItem.id)
: -1;
const canMoveUp =
selectedIndexInSiblings > 0 &&
selectedItem != null &&
isDefined(selectedNavigationMenuItemInEditMode);
const canMoveDown =
selectedIndexInSiblings >= 0 &&
selectedIndexInSiblings < siblings.length - 1 &&
selectedItem != null &&
isDefined(selectedNavigationMenuItemInEditMode);
const handleMoveUp = () => {
if (canMoveUp && isDefined(selectedNavigationMenuItemInEditMode)) {
moveUp(selectedNavigationMenuItemInEditMode);
}
};
const handleMoveDown = () => {
if (canMoveDown && isDefined(selectedNavigationMenuItemInEditMode)) {
moveDown(selectedNavigationMenuItemInEditMode);
}
};
const handleRemove = () => {
if (isDefined(selectedNavigationMenuItemInEditMode)) {
remove(selectedNavigationMenuItemInEditMode);
setSelectedNavigationMenuItemInEditMode(null);
closeCommandMenu();
}
};
const handleAddAtOffset = (offset: 0 | 1) => {
if (!isDefined(selectedItem)) return;
const context = getAddMenuItemInsertionContext(
selectedItem,
workspaceNavigationMenuItems,
offset,
);
if (!context) return;
setAddMenuItemInsertionContext(context);
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconPlus,
resetNavigationStack: true,
});
};
const handleAddBefore = () => handleAddAtOffset(0);
const handleAddAfter = () => handleAddAtOffset(1);
return {
canMoveUp,
canMoveDown,
onMoveUp: handleMoveUp,
onMoveDown: handleMoveDown,
onRemove: handleRemove,
onAddBefore: handleAddBefore,
onAddAfter: handleAddAfter,
};
};
@@ -0,0 +1,55 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type GetAvailableObjectMetadataForNewSidebarItemProps = {
objectMetadataItems: ObjectMetadataItem[];
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
objectMetadataIdsWithIndexView: Set<string>;
objectMetadataIdsWithDisplayableViews: Set<string>;
};
type GetAvailableObjectMetadataForNewSidebarItemResult = {
availableObjectMetadataItems: ObjectMetadataItem[];
availableSystemObjectMetadataItems: ObjectMetadataItem[];
objectMetadataItemsWithViews: ObjectMetadataItem[];
availableSystemObjectMetadataItemsForView: ObjectMetadataItem[];
};
export const getAvailableObjectMetadataForNewSidebarItem = ({
objectMetadataItems,
activeNonSystemObjectMetadataItems,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithDisplayableViews,
}: GetAvailableObjectMetadataForNewSidebarItemProps): GetAvailableObjectMetadataForNewSidebarItemResult => {
const availableObjectMetadataItems = [
...activeNonSystemObjectMetadataItems,
].sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const activeSystemObjectMetadataItems = objectMetadataItems
.filter((item) => item.isActive && item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const availableSystemObjectMetadataItems =
activeSystemObjectMetadataItems.filter((item) =>
objectMetadataIdsWithIndexView.has(item.id),
);
const objectMetadataItemsWithViews = objectMetadataItems
.filter(
(item) =>
item.isActive && objectMetadataIdsWithDisplayableViews.has(item.id),
)
.filter((item) => !item.isSystem)
.sort((a, b) => a.labelPlural.localeCompare(b.labelPlural));
const availableSystemObjectMetadataItemsForView =
activeSystemObjectMetadataItems.filter((item) =>
objectMetadataIdsWithDisplayableViews.has(item.id),
);
return {
availableObjectMetadataItems,
availableSystemObjectMetadataItems,
objectMetadataItemsWithViews,
availableSystemObjectMetadataItemsForView,
};
};
@@ -0,0 +1,27 @@
import { CommandMenuNavigationItemActions } from '@/command-menu/pages/navigation-menu-item/command-menu-navigation-item-actions';
const ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER: CommandMenuNavigationItemActions[] =
[
CommandMenuNavigationItemActions.MOVE_UP,
CommandMenuNavigationItemActions.MOVE_DOWN,
CommandMenuNavigationItemActions.ADD_BEFORE,
CommandMenuNavigationItemActions.ADD_AFTER,
CommandMenuNavigationItemActions.REMOVE,
];
const ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER: CommandMenuNavigationItemActions[] =
[
CommandMenuNavigationItemActions.MOVE_UP,
CommandMenuNavigationItemActions.MOVE_DOWN,
CommandMenuNavigationItemActions.MOVE_TO_FOLDER,
CommandMenuNavigationItemActions.ADD_BEFORE,
CommandMenuNavigationItemActions.ADD_AFTER,
CommandMenuNavigationItemActions.REMOVE,
];
export const getOrganizeActionsSelectableItemIds = (
includeMoveToFolder: boolean,
): CommandMenuNavigationItemActions[] =>
includeMoveToFolder
? ORGANIZE_ACTIONS_WITH_MOVE_TO_FOLDER
: ORGANIZE_ACTIONS_WITHOUT_MOVE_TO_FOLDER;
@@ -24,4 +24,6 @@ export enum CommandMenuPages {
PageLayoutFieldsSettings = 'page-layout-fields-settings',
PageLayoutFieldsLayout = 'page-layout-fields-layout',
ViewFrontComponent = 'view-front-component',
NavigationMenuItemEdit = 'navigation-menu-item-edit',
NavigationMenuAddItem = 'navigation-menu-add-item',
}
@@ -1,6 +1,5 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites';
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
import { useFavorites } from '@/favorites/hooks/useFavorites';
@@ -69,10 +68,10 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<FavoritesDragProvider>
<>
<FavoriteFolders isNavigationSectionOpen={isNavigationSectionOpen} />
<CurrentWorkspaceMemberOrphanFavorites />
</FavoritesDragProvider>
</>
)}
</NavigationDrawerSection>
);
@@ -0,0 +1,108 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import type { ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { getIconBackgroundColorForPayload } from '@/navigation-menu-item/utils/getIconBackgroundColorForPayload';
const StyledIconSlot = styled.div<{ $hasFixedSize: boolean }>`
align-items: center;
cursor: grab;
display: flex;
flex-shrink: 0;
justify-content: center;
${({ theme, $hasFixedSize }) =>
$hasFixedSize &&
css`
height: ${theme.spacing(4.5)};
width: ${theme.spacing(4.5)};
`}
&:active {
cursor: grabbing;
}
`;
type AddToNavigationDragHandleIconProps = {
icon?: IconComponent;
customIconContent?: ReactNode;
};
const AddToNavigationDragHandleIcon = ({
icon,
customIconContent,
}: AddToNavigationDragHandleIconProps) => {
const theme = useTheme();
const iconSize = theme.icon.size.md;
const iconStroke = theme.icon.stroke.sm;
if (isDefined(customIconContent)) {
return <>{customIconContent}</>;
}
if (isDefined(icon)) {
const Icon = icon;
return (
<Icon size={iconSize} stroke={iconStroke} color={theme.grayScale.gray1} />
);
}
};
type AddToNavigationDragHandleProps = {
icon?: IconComponent;
customIconContent?: ReactNode;
payload: AddToNavigationDragPayload;
isHovered: boolean;
};
export const AddToNavigationDragHandle = ({
icon,
customIconContent,
payload,
isHovered,
}: AddToNavigationDragHandleProps) => {
const theme = useTheme();
const iconBackgroundColor = getIconBackgroundColorForPayload(payload, theme);
const hasBackgroundColor = !!iconBackgroundColor && !isHovered;
const payloadHasBackgroundColor = !!iconBackgroundColor;
const iconSize = theme.icon.size.md;
const iconStroke = theme.icon.stroke.sm;
const showCustomContentWithoutWrapper = isDefined(customIconContent);
return (
<StyledIconSlot
$hasFixedSize={
payloadHasBackgroundColor || showCustomContentWithoutWrapper
}
>
{isHovered ? (
<IconGripVertical
size={iconSize}
stroke={iconStroke}
color={theme.font.color.tertiary}
/>
) : showCustomContentWithoutWrapper ? (
customIconContent
) : hasBackgroundColor ? (
<StyledNavigationMenuItemIconContainer
$backgroundColor={iconBackgroundColor}
>
<AddToNavigationDragHandleIcon
icon={icon}
customIconContent={customIconContent}
/>
</StyledNavigationMenuItemIconContainer>
) : (
<AddToNavigationDragHandleIcon
icon={icon}
customIconContent={customIconContent}
/>
)}
</StyledIconSlot>
);
};
@@ -6,7 +6,6 @@ import { LightIconButton } from 'twenty-ui/input';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
import { NavigationMenuItemDragProvider } from '@/navigation-menu-item/components/NavigationMenuItemDragProvider';
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
@@ -21,7 +20,7 @@ import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/us
export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
const [
isNavigationMenuItemFolderCreating,
@@ -51,7 +50,8 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
if (
(!navigationMenuItemsSorted || navigationMenuItemsSorted.length === 0) &&
!isNavigationMenuItemFolderCreating &&
(!navigationMenuItemsByFolder || navigationMenuItemsByFolder.length === 0)
(!userNavigationMenuItemsByFolder ||
userNavigationMenuItemsByFolder.length === 0)
) {
return null;
}
@@ -72,12 +72,12 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
/>
</NavigationDrawerAnimatedCollapseWrapper>
{isNavigationSectionOpen && (
<NavigationMenuItemDragProvider>
<>
<NavigationMenuItemFolders
isNavigationSectionOpen={isNavigationSectionOpen}
/>
<CurrentWorkspaceMemberOrphanNavigationMenuItems />
</NavigationMenuItemDragProvider>
</>
)}
</NavigationDrawerSection>
);
@@ -1,18 +1,26 @@
import { useTheme } from '@emotion/react';
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { useContext, useState } from 'react';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { createPortal } from 'react-dom';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { LightIconButton } from 'twenty-ui/input';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useIsMobile } from 'twenty-ui/utilities';
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/components/NavigationMenuItemFolderNavigationDrawerItemDropdown';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
@@ -35,24 +43,32 @@ import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/compo
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
type CurrentWorkspaceMemberNavigationMenuItemsProps = {
folder: {
folderId: string;
id: string;
folderName: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
};
isGroup: boolean;
isWorkspaceFolder?: boolean;
};
export const CurrentWorkspaceMemberNavigationMenuItems = ({
folder,
isGroup,
isWorkspaceFolder = false,
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
const { t } = useLingui();
const theme = useTheme();
const iconColors = getNavigationMenuItemIconColors(theme);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentPath = useLocation().pathname;
const currentViewPath = useLocation().pathname + useLocation().search;
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
const currentViewPath = location.pathname + location.search;
const { isDragging } = useContext(NavigationMenuItemDragContext);
const [
isNavigationMenuItemFolderRenaming,
@@ -71,22 +87,31 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
currentNavigationMenuItemFolderIdState,
);
const isOpen = openNavigationMenuItemFolderIds.includes(folder.folderId);
const isOpen = openNavigationMenuItemFolderIds.includes(folder.id);
const handleToggle = () => {
if (isMobile) {
setCurrentFolderId((prev) =>
prev === folder.folderId ? null : folder.folderId,
);
setCurrentFolderId((prev) => (prev === folder.id ? null : folder.id));
} else {
setOpenNavigationMenuItemFolderIds((currentOpenFolders) => {
if (isOpen) {
return currentOpenFolders.filter((id) => id !== folder.folderId);
return currentOpenFolders.filter((id) => id !== folder.id);
} else {
return [...currentOpenFolders, folder.folderId];
return [...currentOpenFolders, folder.id];
}
});
}
if (!isOpen) {
const firstNonLinkItem = folder.navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
}
}
};
const { renameNavigationMenuItemFolder } =
@@ -94,7 +119,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
const { deleteNavigationMenuItemFolder } =
useDeleteNavigationMenuItemFolder();
const dropdownId = `navigation-menu-item-folder-edit-${folder.folderId}`;
const dropdownId = `navigation-menu-item-folder-edit-${folder.id}`;
const isDropdownOpenComponent = useRecoilComponentValue(
isDropdownOpenComponentState,
@@ -109,13 +134,15 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
);
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
const folderContentDropDisabled =
useIsDropDisabledForSection(isWorkspaceFolder);
const navigationMenuItemFolderContentLength =
folder.navigationMenuItems.length;
const handleSubmitRename = async (value: string) => {
if (value === '') return;
await renameNavigationMenuItemFolder(folder.folderId, value);
await renameNavigationMenuItemFolder(folder.id, value);
setIsNavigationMenuItemFolderRenaming(false);
return true;
};
@@ -134,29 +161,29 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
return;
}
await renameNavigationMenuItemFolder(folder.folderId, value);
await renameNavigationMenuItemFolder(folder.id, value);
setIsNavigationMenuItemFolderRenaming(false);
};
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.folderId}`;
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.id}`;
const handleNavigationMenuItemFolderDelete = async () => {
if (folder.navigationMenuItems.length > 0) {
openModal(modalId);
closeDropdown(dropdownId);
} else {
await deleteNavigationMenuItemFolder(folder.folderId);
await deleteNavigationMenuItemFolder(folder.id);
closeDropdown(dropdownId);
}
};
const handleConfirmDelete = async () => {
await deleteNavigationMenuItemFolder(folder.folderId);
await deleteNavigationMenuItemFolder(folder.id);
};
const rightOptions = (
const rightOptions = isWorkspaceFolder ? undefined : (
<NavigationMenuItemFolderNavigationDrawerItemDropdown
folderId={folder.folderId}
folderId={folder.id}
onRename={() => setIsNavigationMenuItemFolderRenaming(true)}
onDelete={handleNavigationMenuItemFolderDelete}
closeDropdown={() => {
@@ -175,7 +202,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
return (
<>
<NavigationDrawerItemsCollapsableContainer
key={folder.folderId}
key={folder.id}
isGroup={isGroup}
>
{isNavigationMenuItemFolderRenaming ? (
@@ -189,18 +216,30 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
/>
) : (
<NavigationMenuItemDroppable
droppableId={`folder-header-${folder.folderId}`}
droppableId={`folder-header-${folder.id}`}
isWorkspaceSection={isWorkspaceFolder}
>
<NavigationDrawerItem
label={folder.folderName}
Icon={isOpen ? IconFolderOpen : IconFolder}
onClick={handleToggle}
rightOptions={rightOptions}
className="navigation-drawer-item"
isRightOptionsDropdownOpen={isDropdownOpenComponent}
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
/>
<NavigationItemDropTarget
folderId={folder.id}
index={0}
sectionId={
isWorkspaceFolder
? NavigationSections.WORKSPACE
: NavigationSections.FAVORITES
}
>
<NavigationDrawerItem
label={folder.folderName}
Icon={isOpen ? IconFolderOpen : IconFolder}
iconBackgroundColor={iconColors.folder}
onClick={handleToggle}
rightOptions={rightOptions}
className="navigation-drawer-item"
isRightOptionsDropdownOpen={isDropdownOpenComponent}
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
/>
</NavigationItemDropTarget>
</NavigationMenuItemDroppable>
)}
@@ -210,7 +249,10 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
mode="fit-content"
containAnimation
>
<Droppable droppableId={`folder-${folder.folderId}`}>
<Droppable
droppableId={`folder-${folder.id}`}
isDropDisabled={folderContentDropDisabled}
>
{(provided) => (
<div
ref={provided.innerRef}
@@ -244,13 +286,15 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
selectedIndex: selectedNavigationMenuItemIndex,
})}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
accent="tertiary"
/>
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
accent="tertiary"
/>
)
}
isDragging={isDragging}
triggerEvent="CLICK"
@@ -5,9 +5,11 @@ import { useRecoilValue } from 'recoil';
import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
@@ -39,51 +41,67 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
return (
<NavigationMenuItemDroppable
droppableId={ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID}
droppableId={
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS
}
isWorkspaceSection={false}
>
{orphanNavigationMenuItems.length > 0 ? (
orphanNavigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem,
)}
to={isDragging ? undefined : navigationMenuItem.link}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
<>
{orphanNavigationMenuItems.map((navigationMenuItem, index) => (
<NavigationItemDropTarget
key={navigationMenuItem.id}
folderId={null}
index={index}
sectionId={NavigationSections.FAVORITES}
>
<DraggableItem
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem,
)}
to={isDragging ? undefined : navigationMenuItem.link}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
accent="tertiary"
/>
}
accent="tertiary"
isDragging={isDragging}
triggerEvent="CLICK"
/>
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
</StyledOrphanNavigationMenuItemsContainer>
}
</StyledOrphanNavigationMenuItemsContainer>
}
/>
</NavigationItemDropTarget>
))}
<NavigationItemDropTarget
folderId={null}
index={orphanNavigationMenuItems.length}
sectionId={NavigationSections.FAVORITES}
/>
))
</>
) : (
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
)}
@@ -0,0 +1,29 @@
import { useTheme } from '@emotion/react';
import type { IconComponent } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
type IconWithBackgroundProps = {
Icon: IconComponent;
backgroundColor: string;
size?: number | string;
stroke?: number | string;
};
export const IconWithBackground = ({
Icon,
backgroundColor,
size,
stroke,
}: IconWithBackgroundProps) => {
const theme = useTheme();
return (
<StyledNavigationMenuItemIconContainer $backgroundColor={backgroundColor}>
<Icon
size={size ?? theme.icon.size.md}
stroke={stroke ?? theme.icon.stroke.md}
color={theme.grayScale.gray1}
/>
</StyledNavigationMenuItemIconContainer>
);
};
@@ -0,0 +1,83 @@
import styled from '@emotion/styled';
import { type ReactNode, useContext } from 'react';
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;
}>`
min-height: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
position: relative;
transition: all 150ms ease-in-out;
${({ $isDragOver, theme }) =>
$isDragOver &&
`
background-color: ${theme.background.transparent.blue};
&::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: ${theme.color.blue};
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
}
`}
${({ $isDropForbidden, theme }) =>
$isDropForbidden &&
`
background-color: ${theme.background.transparent.danger};
cursor: not-allowed;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: ${theme.color.red};
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
}
`}
`;
type NavigationItemDropTargetProps = {
folderId: string | null;
index: number;
sectionId: NavigationSections;
children?: ReactNode;
compact?: boolean;
};
export const NavigationItemDropTarget = ({
folderId,
index,
sectionId,
children,
compact = false,
}: NavigationItemDropTargetProps) => {
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
NavigationDropTargetContext,
);
const dropTargetId = `${sectionId}-${folderId ?? 'orphan'}-${index}`;
const isDragOver = activeDropTargetId === dropTargetId;
const isDropForbidden = forbiddenDropTargetId === dropTargetId;
return (
<StyledDropTarget
$isDragOver={isDragOver}
$isDropForbidden={isDropForbidden}
$compact={compact}
>
{children}
</StyledDropTarget>
);
};
@@ -0,0 +1,110 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconCheck, useIcons } from 'twenty-ui/display';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledContainer = styled.div`
align-items: center;
background: ${({ theme }) => theme.color.blue};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.inverted};
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(2, 3)};
width: 100%;
`;
const StyledTitle = styled.span`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
export const NavigationMenuEditModeBar = () => {
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const [isSaving, setIsSaving] = useState(false);
const { closeCommandMenu } = useCommandMenu();
const { enqueueErrorSnackBar } = useSnackBar();
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const setIsNavigationMenuInEditMode = useSetRecoilState(
isNavigationMenuInEditModeState,
);
const { saveDraft } = useSaveNavigationMenuItemsDraft();
const { isDirty } = useNavigationMenuItemsDraftState();
const cancelEditMode = () => {
setNavigationMenuItemsDraft(null);
setSelectedNavigationMenuItemInEditMode(null);
setIsNavigationMenuInEditMode(false);
};
const isNavigationMenuInEditMode = useRecoilValue(
isNavigationMenuInEditModeState,
);
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const showNavigationMenuEditModeBar =
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
const handleSave = async () => {
if (!isDirty) return;
setIsSaving(true);
try {
await saveDraft();
cancelEditMode();
closeCommandMenu();
} catch {
enqueueErrorSnackBar({
message: t`Failed to save navigation layout`,
});
} finally {
setIsSaving(false);
}
};
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>
);
};
@@ -1,11 +1,14 @@
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
type NavigationMenuItemDroppableProps = {
droppableId: string;
children: React.ReactNode;
isDragIndicatorVisible?: boolean;
showDropLine?: boolean;
isWorkspaceSection?: boolean;
};
const StyledDroppableWrapper = styled.div<{
@@ -46,9 +49,12 @@ export const NavigationMenuItemDroppable = ({
children,
isDragIndicatorVisible = true,
showDropLine = true,
isWorkspaceSection = false,
}: NavigationMenuItemDroppableProps) => {
const isDropDisabled = useIsDropDisabledForSection(isWorkspaceSection);
return (
<Droppable droppableId={droppableId}>
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
{(provided, snapshot) => (
<StyledDroppableWrapper
isDraggingOver={snapshot.isDraggingOver}
@@ -4,7 +4,6 @@ import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/components/NavigationMenuItemBackButton';
import { NavigationMenuItemDragProvider } from '@/navigation-menu-item/components/NavigationMenuItemDragProvider';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
@@ -30,53 +29,51 @@ export const NavigationMenuItemFolderContent = ({
return (
<>
<NavigationMenuItemBackButton folderName={folderName} />
<NavigationMenuItemDragProvider>
<Droppable droppableId={`folder-${folderId}`}>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={navigationMenuItem.link}
/>
}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</NavigationMenuItemDragProvider>
<Droppable droppableId={`folder-${folderId}`}>
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={() =>
deleteNavigationMenuItem(navigationMenuItem.id)
}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={navigationMenuItem.link}
/>
}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</>
);
};
@@ -18,7 +18,7 @@ export const NavigationMenuItemFolders = ({
}: NavigationMenuItemFoldersProps) => {
const [newFolderName, setNewFolderName] = useState('');
const { navigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
const { createNewNavigationMenuItemFolder } =
useCreateNavigationMenuItemFolder();
@@ -79,11 +79,11 @@ export const NavigationMenuItemFolders = ({
/>
</NavigationDrawerAnimatedCollapseWrapper>
)}
{navigationMenuItemsByFolder.map((folder) => (
{userNavigationMenuItemsByFolder.map((folder) => (
<CurrentWorkspaceMemberNavigationMenuItems
key={folder.folderId}
key={folder.id}
folder={folder}
isGroup={navigationMenuItemsByFolder.length > 1}
isGroup={userNavigationMenuItemsByFolder.length > 1}
/>
))}
</>
@@ -1,8 +1,16 @@
import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { ViewKey } from '@/views/types/ViewKey';
export const NavigationMenuItemIcon = ({
navigationMenuItem,
@@ -11,19 +19,66 @@ export const NavigationMenuItemIcon = ({
}) => {
const theme = useTheme();
const { getIcon } = useIcons();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
navigationMenuItem.objectNameSingular || '',
);
const isRecord =
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
const isLink = navigationMenuItem.itemType === NavigationMenuItemType.LINK;
const isObjectIndexView =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.viewKey === ViewKey.Index;
const isViewWithOverlay =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
!isObjectIndexView;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
);
const ObjectIconForView =
objectMetadataItem?.icon != null
? getIcon(objectMetadataItem.icon)
: StandardIcon;
if (
isViewWithOverlay &&
isDefined(ObjectIconForView) &&
isDefined(navigationMenuItem.Icon)
) {
const ViewIcon = getIcon(navigationMenuItem.Icon);
return (
<ObjectIconWithViewOverlay
ObjectIcon={ObjectIconForView}
ViewIcon={ViewIcon}
/>
);
}
const IconToUse =
StandardIcon ||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
const iconColorToUse = StandardIcon ? IconColor : theme.font.color.secondary;
const placeholderColorSeed = navigationMenuItem.targetRecordId ?? undefined;
const iconColors = getNavigationMenuItemIconColors(theme);
const iconBackgroundColor = isRecord
? undefined
: isLink
? iconColors.link
: isViewWithOverlay
? iconColors.view
: iconColors.object;
return (
const iconColorToUse = iconBackgroundColor
? theme.grayScale.gray1
: StandardIcon
? IconColor
: theme.font.color.secondary;
const avatar = (
<Avatar
size="md"
size={iconBackgroundColor ? 'sm' : 'md'}
type={navigationMenuItem.avatarType}
Icon={IconToUse}
iconColor={iconColorToUse}
@@ -32,4 +87,16 @@ export const NavigationMenuItemIcon = ({
placeholderColorSeed={placeholderColorSeed}
/>
);
if (!iconBackgroundColor) {
return avatar;
}
return (
<StyledNavigationMenuItemIconContainer
$backgroundColor={iconBackgroundColor}
>
{avatar}
</StyledNavigationMenuItemIconContainer>
);
};
@@ -0,0 +1,16 @@
import styled from '@emotion/styled';
export const StyledNavigationMenuItemIconContainer = styled.div<{
$backgroundColor?: string;
}>`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.spacing(4.5)};
justify-content: center;
width: ${({ theme }) => theme.spacing(4.5)};
${({ $backgroundColor }) =>
$backgroundColor ? `background-color: ${$backgroundColor};` : ''}
`;
@@ -0,0 +1,72 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import type { IconComponent } from 'twenty-ui/display';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
const StyledCompositeContainer = styled.div`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
display: flex;
flex-shrink: 0;
height: ${({ theme }) => theme.spacing(4.5)};
justify-content: center;
position: relative;
width: ${({ theme }) => theme.spacing(4.5)};
`;
const StyledObjectIconWrapper = styled.div<{ $backgroundColor: string }>`
position: absolute;
inset: 0;
border-radius: ${({ theme }) => theme.border.radius.xs};
background-color: ${({ $backgroundColor }) => $backgroundColor};
display: flex;
align-items: center;
justify-content: center;
`;
const StyledViewOverlay = styled.div<{ $backgroundColor: string }>`
align-items: center;
background-color: ${({ $backgroundColor }) => $backgroundColor};
border: 1px solid ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.xs};
bottom: -7px;
display: flex;
height: ${({ theme }) => theme.spacing(3.5)};
justify-content: center;
position: absolute;
right: -7px;
width: ${({ theme }) => theme.spacing(3.5)};
`;
export type ObjectIconWithViewOverlayProps = {
ObjectIcon: IconComponent;
ViewIcon: IconComponent;
};
export const ObjectIconWithViewOverlay = ({
ObjectIcon,
ViewIcon,
}: ObjectIconWithViewOverlayProps) => {
const theme = useTheme();
const iconColors = getNavigationMenuItemIconColors(theme);
return (
<StyledCompositeContainer>
<StyledObjectIconWrapper $backgroundColor={iconColors.object}>
<ObjectIcon
size={theme.spacing(3.5)}
stroke={theme.icon.stroke.md}
color={theme.grayScale.gray1}
/>
</StyledObjectIconWrapper>
<StyledViewOverlay $backgroundColor={iconColors.view}>
<ViewIcon
size={theme.spacing(2)}
stroke={theme.icon.stroke.sm}
color={theme.grayScale.gray1}
/>
</StyledViewOverlay>
</StyledCompositeContainer>
);
};
@@ -0,0 +1,83 @@
import { useTheme } from '@emotion/react';
import {
type DraggableProvided,
type DraggableRubric,
type DraggableStateSnapshot,
} from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
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 { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
draggableProvided: DraggableProvided;
draggableSnapshot: DraggableStateSnapshot;
rubric: DraggableRubric;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItemFolderContentLength: number;
selectedNavigationMenuItemIndex: number;
};
export const WorkspaceNavigationMenuItemFolderDragClone = ({
draggableProvided,
draggableSnapshot,
rubric,
navigationMenuItems,
navigationMenuItemFolderContentLength,
selectedNavigationMenuItemIndex,
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
const theme = useTheme();
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const navigationMenuItem = navigationMenuItems[rubric.source.index];
if (!isDefined(navigationMenuItem)) {
return null;
}
return (
<div
ref={draggableProvided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
// eslint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.dragHandleProps}
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
: undefined,
}}
>
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.Index
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})
}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
to={undefined}
active={false}
isDragging={true}
subItemState={getNavigationSubItemLeftAdornment({
index: rubric.source.index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
triggerEvent="CLICK"
/>
</div>
);
};
@@ -1,26 +1,183 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import {
useRecoilCallback,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { IconLink, IconPlus, IconTool, useIcons } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/hooks/useWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
import {
type NavigationMenuItemClickParams,
useWorkspaceSectionItems,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
const StyledRightIconsContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const WorkspaceNavigationMenuItems = () => {
const { workspaceNavigationMenuItemsObjectMetadataItems } =
useWorkspaceNavigationMenuItems();
const items = useWorkspaceSectionItems();
const enterEditMode = useRecoilCallback(
({ set, snapshot }) =>
() => {
const prefetchNavigationMenuItems = snapshot
.getLoadable(prefetchNavigationMenuItemsState)
.getValue();
const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
prefetchNavigationMenuItems,
);
set(navigationMenuItemsDraftState, workspaceNavigationMenuItems);
set(isNavigationMenuInEditModeState, true);
},
[],
);
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const isNavigationMenuInEditMode = useRecoilValue(
isNavigationMenuInEditModeState,
);
const [
selectedNavigationMenuItemInEditMode,
setSelectedNavigationMenuItemInEditMode,
] = useRecoilState(selectedNavigationMenuItemInEditModeState);
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
openNavigationMenuItemFolderIdsState,
);
const { navigateCommandMenu } = useNavigateCommandMenu();
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
const { getIcon } = useIcons();
const loading = useIsPrefetchLoading();
const { t } = useLingui();
const handleEditClick = (event: React.MouseEvent) => {
event.stopPropagation();
enterEditMode();
};
const handleNavigationMenuItemClick = (
params: NavigationMenuItemClickParams,
) => {
const { item, objectMetadataItem } = params;
const id = item.id;
setSelectedNavigationMenuItemInEditMode(id);
if (item.itemType === NavigationMenuItemType.FOLDER) {
setOpenNavigationMenuItemFolderIds((currentOpenFolders) =>
currentOpenFolders.includes(id)
? currentOpenFolders
: [...currentOpenFolders, id],
);
openNavigationMenuItemInCommandMenu({
pageTitle: t`Edit folder`,
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
});
} else if (item.itemType === NavigationMenuItemType.LINK) {
openNavigationMenuItemInCommandMenu({
pageTitle: t`Edit link`,
pageIcon: IconLink,
});
} else if (isDefined(objectMetadataItem)) {
openNavigationMenuItemInCommandMenu({
pageTitle: objectMetadataItem.labelPlural,
pageIcon: getIcon(objectMetadataItem.icon),
});
}
};
const handleActiveObjectMetadataItemClick = (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => {
enterEditMode();
setSelectedNavigationMenuItemInEditMode(navigationMenuItemId);
openNavigationMenuItemInCommandMenu({
pageTitle: objectMetadataItem.labelPlural,
pageIcon: getIcon(objectMetadataItem.icon),
});
};
const handleAddMenuItem = (event?: React.MouseEvent) => {
event?.stopPropagation();
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconPlus,
resetNavigationStack: true,
});
};
const isEditMode =
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
if (loading) {
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
}
return (
<NavigationDrawerSectionForObjectMetadataItems
<NavigationDrawerSectionForWorkspaceItems
sectionTitle={t`Workspace`}
objectMetadataItems={workspaceNavigationMenuItemsObjectMetadataItems}
isRemote={false}
items={items}
rightIcon={
isNavigationMenuItemEditingEnabled ? (
<StyledRightIconsContainer>
{isEditMode ? (
<LightIconButton
Icon={IconPlus}
accent="tertiary"
size="small"
onClick={handleAddMenuItem}
/>
) : (
<LightIconButton
Icon={IconTool}
accent="tertiary"
size="small"
onClick={handleEditClick}
/>
)}
</StyledRightIconsContainer>
) : undefined
}
onAddMenuItem={
isNavigationMenuItemEditingEnabled && isEditMode
? handleAddMenuItem
: undefined
}
isEditMode={isEditMode}
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
onNavigationMenuItemClick={
isEditMode ? handleNavigationMenuItemClick : undefined
}
onActiveObjectMetadataItemClick={
isNavigationMenuItemEditingEnabled
? handleActiveObjectMetadataItemClick
: undefined
}
/>
);
};
@@ -0,0 +1,299 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useIsMobile } from 'twenty-ui/utilities';
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { WorkspaceNavigationMenuItemFolderDragClone } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderDragClone';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { 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 { coreViewsState } from '@/views/states/coreViewState';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
border: ${({ theme, $isSelectedInEditMode }) =>
$isSelectedInEditMode
? `1px solid ${theme.color.blue}`
: '1px solid transparent'};
border-radius: ${({ theme }) => theme.border.radius.sm};
`;
const StyledFolderDroppableContent = styled.div<{
$compact: boolean;
}>`
padding-bottom: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
`;
const StyledFolderExpandableWrapper = styled.div`
& > div {
overflow: visible !important;
}
`;
type WorkspaceNavigationMenuItemsFolderProps = {
folderId: string;
folderName: string;
folderIconKey?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
isGroup: boolean;
isEditMode?: boolean;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
selectedNavigationMenuItemId?: string | null;
isDragging?: boolean;
};
export const WorkspaceNavigationMenuItemsFolder = ({
folderId,
folderName,
folderIconKey,
navigationMenuItems,
isGroup,
isEditMode = false,
isSelectedInEditMode = false,
onEditModeClick,
onNavigationMenuItemClick,
selectedNavigationMenuItemId = null,
isDragging = false,
}: WorkspaceNavigationMenuItemsFolderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const iconColors = getNavigationMenuItemIconColors(theme);
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
const currentViewPath = location.pathname + location.search;
const isMobile = useIsMobile();
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useRecoilState(openNavigationMenuItemFolderIdsState);
const setCurrentFolderId = useSetRecoilState(
currentNavigationMenuItemFolderIdState,
);
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
const handleToggle = () => {
if (isMobile) {
setCurrentFolderId((prev) => (prev === folderId ? null : folderId));
} else {
setOpenNavigationMenuItemFolderIds((current) =>
isOpen
? current.filter((id) => id !== folderId)
: [...current, folderId],
);
}
if (!isOpen) {
const firstNonLinkItem = navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
}
}
};
const shouldUseEditModeClick = isEditMode && isDefined(onEditModeClick);
const handleClick = shouldUseEditModeClick ? onEditModeClick : handleToggle;
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
);
const navigationMenuItemFolderContentLength = navigationMenuItems.length;
const { isDragging: isContextDragging } = useContext(
NavigationMenuItemDragContext,
);
const folderContentDropDisabled = useIsDropDisabledForSection(true);
return (
<StyledFolderContainer $isSelectedInEditMode={isSelectedInEditMode}>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<NavigationMenuItemDroppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`}
isWorkspaceSection={true}
>
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconBackgroundColor={iconColors.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}
/>
)
}
alwaysShowRightOptions
/>
</NavigationMenuItemDroppable>
<StyledFolderExpandableWrapper>
<AnimatedExpandableContainer
isExpanded={isOpen}
dimension="height"
mode="fit-content"
containAnimation
>
<Droppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`}
isDropDisabled={folderContentDropDisabled}
ignoreContainerClipping
renderClone={(provided, snapshot, rubric) => (
<WorkspaceNavigationMenuItemFolderDragClone
draggableProvided={provided}
draggableSnapshot={snapshot}
rubric={rubric}
navigationMenuItems={navigationMenuItems}
navigationMenuItemFolderContentLength={
navigationMenuItemFolderContentLength
}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
/>
)}
getContainerForClone={() => document.body}
>
{(provided) => (
<StyledFolderDroppableContent
ref={provided.innerRef}
$compact={isEditMode || navigationMenuItems.length === 0}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => {
const objectMetadataItem =
navigationMenuItem.itemType ===
NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType ===
NavigationMenuItemType.RECORD
? getObjectMetadataForNavigationMenuItem(
navigationMenuItem,
objectMetadataItems,
views,
)
: null;
const handleEditModeClick =
isEditMode &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.itemType ===
NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem))
? () =>
onNavigationMenuItemClick({
item: navigationMenuItem,
objectMetadataItem:
objectMetadataItem ?? undefined,
})
: undefined;
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.Index
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})
}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
selectedNavigationMenuItemId ===
navigationMenuItem.id
}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength:
navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
triggerEvent="CLICK"
/>
}
/>
);
})}
{provided.placeholder}
</StyledFolderDroppableContent>
)}
</Droppable>
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
</StyledFolderContainer>
);
};
@@ -0,0 +1 @@
export const ADD_TO_NAV_SOURCE_DROPPABLE_ID = 'add-to-nav-source';
@@ -1,2 +1,6 @@
export const ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID =
'orphan-navigation-menu-items';
export enum NavigationMenuItemDroppableIds {
ORPHAN_NAVIGATION_MENU_ITEMS = 'orphan-navigation-menu-items',
WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS = 'workspace-orphan-navigation-menu-items',
WORKSPACE_FOLDER_PREFIX = 'workspace-folder-',
WORKSPACE_FOLDER_HEADER_PREFIX = 'workspace-folder-header-',
}
@@ -0,0 +1,7 @@
export enum NavigationMenuItemType {
FOLDER = 'folder',
LINK = 'link',
OBJECT = 'object',
RECORD = 'record',
VIEW = 'view',
}
@@ -0,0 +1,4 @@
export enum NavigationSections {
WORKSPACE = 'workspace',
FAVORITES = 'favorites',
}
@@ -0,0 +1 @@
export const FOLDER_ICON_DEFAULT = 'IconFolder';
@@ -0,0 +1,10 @@
import { createContext } from 'react';
type NavigationDragSourceContextType = {
sourceDroppableId: string | null;
};
export const NavigationDragSourceContext =
createContext<NavigationDragSourceContextType>({
sourceDroppableId: null,
});
@@ -0,0 +1,16 @@
import { createContext } from 'react';
type NavigationDropTargetContextType = {
activeDropTargetId: string | null;
setActiveDropTargetId: (id: string | null) => void;
forbiddenDropTargetId: string | null;
setForbiddenDropTargetId: (id: string | null) => void;
};
export const NavigationDropTargetContext =
createContext<NavigationDropTargetContextType>({
activeDropTargetId: null,
setActiveDropTargetId: () => {},
forbiddenDropTargetId: null,
setForbiddenDropTargetId: () => {},
});
@@ -9,6 +9,8 @@ export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
viewId
folderId
name
link
icon
position
applicationId
createdAt
@@ -0,0 +1,61 @@
import { useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
export const useAddFolderToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const addFolderToDraft = (
name: string,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string => {
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
viewId: undefined,
targetObjectMetadataId: undefined,
targetRecordId: undefined,
folderId: folderId ?? undefined,
position,
userWorkspaceId: undefined,
name: name.trim(),
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addFolderToDraft };
};
@@ -0,0 +1,65 @@
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
import { normalizeUrl } from '@/navigation-menu-item/utils/normalizeUrl';
export const useAddLinkToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const addLinkToDraft = (
label: string,
url: string,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string => {
const normalizedUrl = normalizeUrl(url);
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
viewId: undefined,
targetObjectMetadataId: undefined,
targetRecordId: undefined,
folderId: folderId ?? undefined,
position,
userWorkspaceId: undefined,
name: label.trim() || 'Link',
link: normalizedUrl,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addLinkToDraft };
};
@@ -0,0 +1,62 @@
import { useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
export const useAddObjectToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const addObjectToDraft = (
objectMetadataId: string,
defaultViewId: string,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string => {
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
viewId: defaultViewId,
targetObjectMetadataId: objectMetadataId,
position,
userWorkspaceId: undefined,
targetRecordId: undefined,
folderId: folderId ?? undefined,
name: undefined,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addObjectToDraft };
};
@@ -0,0 +1,89 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
type SearchRecord = {
recordId: string;
objectNameSingular: string;
label: string;
imageUrl?: string | null;
};
type SearchRecordWithOptionalMetadataId = SearchRecord & {
objectMetadataId?: string;
};
export const useAddRecordToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const addRecordToDraft = (
searchRecord: SearchRecordWithOptionalMetadataId,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string | undefined => {
const objectMetadataId =
searchRecord.objectMetadataId ??
objectMetadataItems.find(
(item) => item.nameSingular === searchRecord.objectNameSingular,
)?.id;
if (!isDefined(objectMetadataId)) {
return undefined;
}
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
viewId: undefined,
targetObjectMetadataId: objectMetadataId,
targetRecordId: searchRecord.recordId,
targetRecordIdentifier: {
id: searchRecord.recordId,
labelIdentifier: searchRecord.label,
imageIdentifier: searchRecord.imageUrl ?? null,
},
position,
userWorkspaceId: undefined,
folderId: folderId ?? undefined,
name: undefined,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addRecordToDraft };
};
@@ -0,0 +1,61 @@
import { useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/utils/computeInsertIndexAndPosition';
export const useAddViewToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const addViewToDraft = (
viewId: string,
currentDraft: NavigationMenuItem[],
targetFolderId?: string | null,
targetIndex?: number,
): string => {
const folderId = targetFolderId ?? null;
const itemsInFolder = currentDraft.filter(
(item) =>
(item.folderId ?? null) === folderId &&
!isDefined(item.userWorkspaceId),
);
const index = targetIndex ?? itemsInFolder.length;
const { flatIndex, position } = computeInsertIndexAndPosition(
currentDraft,
folderId,
index,
);
const newItemId = v4();
const newItem: NavigationMenuItem = {
__typename: 'NavigationMenuItem',
id: newItemId,
viewId,
targetObjectMetadataId: undefined,
position,
userWorkspaceId: undefined,
targetRecordId: undefined,
folderId: folderId ?? undefined,
name: undefined,
applicationId: undefined,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const newDraft = [
...currentDraft.slice(0, flatIndex),
newItem,
...currentDraft.slice(flatIndex),
];
setNavigationMenuItemsDraft(newDraft);
return newItemId;
};
return { addViewToDraft };
};
@@ -0,0 +1,15 @@
import { useRecoilValue } from 'recoil';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
export const useDraftNavigationMenuItems = () => {
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const currentDraft = navigationMenuItemsDraft ?? workspaceNavigationMenuItems;
return { currentDraft };
};
@@ -0,0 +1,15 @@
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/hooks/useDraftNavigationMenuItems';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
export const useDraftNavigationMenuItemsAllFolders = () => {
const { currentDraft } = useDraftNavigationMenuItems();
const allFolders =
currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({
id: item.id,
name: item.name ?? 'Folder',
folderId: item.folderId ?? undefined,
})) ?? [];
return { allFolders };
};
@@ -0,0 +1,15 @@
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
export const useDraftNavigationMenuItemsWorkspaceFolders = () => {
const { workspaceNavigationMenuItemsByFolder } =
useNavigationMenuItemsByFolder();
const workspaceFolders = workspaceNavigationMenuItemsByFolder.map(
(folder) => ({
id: folder.id,
name: folder.folderName,
}),
);
return { workspaceFolders };
};
@@ -0,0 +1,213 @@
import type { DropResult, ResponderProvided } from '@hello-pangea/dnd';
import { t } from '@lingui/core/macro';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddRecordToNavigationMenuDraft';
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddViewToNavigationMenuDraft';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/useNavigationMenuItemsDraftState';
import { useOpenNavigationMenuItemInCommandMenu } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInCommandMenu';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/states/addToNavPayloadRegistryState';
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 { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
export const useHandleAddToNavigationDrop = () => {
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const { openNavigationMenuItemInCommandMenu } =
useOpenNavigationMenuItemInCommandMenu();
const { objectMetadataItems } = useObjectMetadataItems();
const coreViews = useRecoilValue(coreViewsState);
const { getIcon } = useIcons();
const setSelectedNavigationMenuItemInEditMode = useSetRecoilState(
selectedNavigationMenuItemInEditModeState,
);
const setIsNavigationMenuInEditMode = useSetRecoilState(
isNavigationMenuInEditModeState,
);
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
openNavigationMenuItemFolderIdsState,
);
const handleAddToNavigationDrop = useRecoilCallback(
({ snapshot }) =>
(result: DropResult, _provided: ResponderProvided) => {
const { source, destination, draggableId } = result;
if (
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
!destination ||
!isWorkspaceDroppableId(destination.droppableId)
) {
return;
}
const payload =
getSnapshotValue(snapshot, addToNavPayloadRegistryState).get(
draggableId,
) ?? null;
if (!payload) {
return;
}
const currentDraft = isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItems;
const folderId = validateAndExtractWorkspaceFolderId(
destination.droppableId,
);
const index = destination.index;
if (
payload.type === NavigationMenuItemType.FOLDER &&
folderId !== null
) {
return;
}
if (isDefined(folderId)) {
setOpenNavigationMenuItemFolderIds((current) =>
current.includes(folderId) ? current : [...current, folderId],
);
}
const openEditForNewNavItem = (
newItemId: string,
options: Parameters<typeof openNavigationMenuItemInCommandMenu>[0],
) => {
setIsNavigationMenuInEditMode(true);
setSelectedNavigationMenuItemInEditMode(newItemId);
openNavigationMenuItemInCommandMenu(options);
};
switch (payload.type) {
case NavigationMenuItemType.FOLDER: {
const newFolderId = addFolderToDraft(
payload.name,
currentDraft,
null,
index,
);
openEditForNewNavItem(newFolderId, {
pageTitle: t`Edit folder`,
pageIcon: IconFolder,
focusTitleInput: true,
});
return;
}
case NavigationMenuItemType.LINK: {
const newLinkId = addLinkToDraft(
payload.name || t`Link label`,
payload.link,
currentDraft,
folderId,
index,
);
openEditForNewNavItem(newLinkId, {
pageTitle: t`Edit link`,
pageIcon: IconLink,
focusTitleInput: true,
});
return;
}
case NavigationMenuItemType.OBJECT: {
const newItemId = addObjectToDraft(
payload.objectMetadataId,
payload.defaultViewId,
currentDraft,
folderId,
index,
);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === payload.objectMetadataId,
);
openEditForNewNavItem(newItemId, {
pageTitle: objectMetadataItem?.labelPlural ?? payload.label,
pageIcon: objectMetadataItem
? getIcon(objectMetadataItem.icon)
: IconFolder,
});
return;
}
case NavigationMenuItemType.VIEW: {
const newItemId = addViewToDraft(
payload.viewId,
currentDraft,
folderId,
index,
);
const views = coreViews.map(convertCoreViewToView);
const view = views.find((v) => v.id === payload.viewId);
openEditForNewNavItem(newItemId, {
pageTitle: view?.name ?? payload.label,
pageIcon: view ? getIcon(view.icon) : IconFolder,
});
return;
}
case NavigationMenuItemType.RECORD: {
const newItemId = addRecordToDraft(
{
recordId: payload.recordId,
objectMetadataId: payload.objectMetadataId,
objectNameSingular: payload.objectNameSingular,
label: payload.label,
imageUrl: payload.imageUrl,
},
currentDraft,
folderId,
index,
);
if (!isDefined(newItemId)) return;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === payload.objectMetadataId,
);
openEditForNewNavItem(newItemId, {
pageTitle: payload.label,
pageIcon: objectMetadataItem
? getIcon(objectMetadataItem.icon)
: IconFolder,
});
return;
}
}
},
[
addFolderToDraft,
addLinkToDraft,
addObjectToDraft,
addRecordToDraft,
addViewToDraft,
coreViews,
getIcon,
navigationMenuItemsDraft,
objectMetadataItems,
openNavigationMenuItemInCommandMenu,
setOpenNavigationMenuItemFolderIds,
setIsNavigationMenuInEditMode,
setSelectedNavigationMenuItemInEditMode,
workspaceNavigationMenuItems,
],
);
return { handleAddToNavigationDrop };
};
@@ -1,7 +1,8 @@
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useSetRecoilState } from 'recoil';
import { ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/hooks/useUpdateNavigationMenuItem';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
@@ -48,6 +49,10 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
return;
}
if (isWorkspaceDroppableId(destination.droppableId)) {
return;
}
const draggedNavigationMenuItem = navigationMenuItems.find(
(item) => item.id === draggableId,
);
@@ -57,11 +62,13 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
const destinationFolderId = validateAndExtractFolderId({
droppableId: destination.droppableId,
orphanDroppableId: ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID,
orphanDroppableId:
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
});
const sourceFolderId = validateAndExtractFolderId({
droppableId: source.droppableId,
orphanDroppableId: ORPHAN_NAVIGATION_MENU_ITEMS_DROPPABLE_ID,
orphanDroppableId:
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
});
if (
@@ -0,0 +1,177 @@
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
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 {
matchesWorkspaceFolderId,
validateAndExtractWorkspaceFolderId,
} from '@/navigation-menu-item/utils/validateAndExtractWorkspaceFolderId';
import { isDefined } from 'twenty-shared/utils';
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
const { workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const isNavigationMenuInEditMode = useRecoilValue(
isNavigationMenuInEditModeState,
);
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const setOpenNavigationMenuItemFolderIds = useSetRecoilState(
openNavigationMenuItemFolderIdsState,
);
const openDestinationFolder = (folderId: string | null) => {
if (!folderId) {
return;
}
setOpenNavigationMenuItemFolderIds((current) => {
if (!current.includes(folderId)) {
return [...current, folderId];
}
return current;
});
};
const handleWorkspaceNavigationMenuItemDragAndDrop: OnDragEndResponder = (
result,
) => {
const { destination, source, draggableId } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const isWorkspaceDrop =
source.droppableId.startsWith('workspace-') &&
destination.droppableId.startsWith('workspace-');
if (!isWorkspaceDrop) {
return;
}
if (!isNavigationMenuInEditMode || !navigationMenuItemsDraft) {
return;
}
const draggedItem = workspaceNavigationMenuItems.find(
(item) => item.id === draggableId,
);
if (!draggedItem) {
return;
}
const destinationFolderId = validateAndExtractWorkspaceFolderId(
destination.droppableId,
);
const sourceFolderId = validateAndExtractWorkspaceFolderId(
source.droppableId,
);
const isDropOnFolderHeader = destination.droppableId.startsWith(
NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX,
);
if (isDropOnFolderHeader && isDefined(destinationFolderId)) {
openDestinationFolder(destinationFolderId);
}
const sourceList = (navigationMenuItemsDraft ?? [])
.filter((item) => matchesWorkspaceFolderId(item, sourceFolderId))
.sort((a, b) => a.position - b.position);
const destinationList = (navigationMenuItemsDraft ?? [])
.filter((item) => matchesWorkspaceFolderId(item, destinationFolderId))
.sort((a, b) => a.position - b.position);
if (!sourceList.some((item) => item.id === draggableId)) {
return;
}
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,
);
sourceListWithoutDragged.forEach((item, index) => {
positionUpdates.set(item.id, {
position: index,
folderId: sourceFolderId,
});
});
}
const updatedDraft = navigationMenuItemsDraft.map(
(item): NavigationMenuItem => {
const update = positionUpdates.get(item.id);
if (!update) return item;
return {
...item,
position: update.position,
folderId: update.folderId,
};
},
);
setNavigationMenuItemsDraft(updatedDraft);
};
return { handleWorkspaceNavigationMenuItemDragAndDrop };
};
@@ -0,0 +1,16 @@
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
import { isWorkspaceDroppableId } from '@/navigation-menu-item/utils/isWorkspaceDroppableId';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useIsDropDisabledForSection = (isWorkspaceSection: boolean) => {
const { sourceDroppableId } = useContext(NavigationDragSourceContext);
if (!isDefined(sourceDroppableId)) {
return false;
}
if (sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
return !isWorkspaceSection;
}
return isWorkspaceDroppableId(sourceDroppableId) !== isWorkspaceSection;
};
@@ -0,0 +1,159 @@
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
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;
});
export const useNavigationMenuItemMoveRemove = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const moveUp = (navigationMenuItemId: string) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
const currentItem = draft.find(
(item) => item.id === navigationMenuItemId,
);
if (!currentItem) return draft;
const folderId = currentItem.folderId ?? null;
const siblings = draft
.filter((item) => (item.folderId ?? null) === folderId)
.sort((a, b) => a.position - b.position);
const currentIndex = siblings.findIndex(
(item) => item.id === navigationMenuItemId,
);
if (currentIndex <= 0) return draft;
const itemAbove = siblings[currentIndex - 1];
return swapPositionsInDraft(draft, currentItem, itemAbove);
});
};
const moveDown = (navigationMenuItemId: string) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
const currentItem = draft.find(
(item) => item.id === navigationMenuItemId,
);
if (!currentItem) return draft;
const folderId = currentItem.folderId ?? null;
const siblings = draft
.filter((item) => (item.folderId ?? null) === folderId)
.sort((a, b) => a.position - b.position);
const currentIndex = siblings.findIndex(
(item) => item.id === navigationMenuItemId,
);
if (currentIndex < 0 || currentIndex >= siblings.length - 1) {
return draft;
}
const itemBelow = siblings[currentIndex + 1];
return swapPositionsInDraft(draft, currentItem, itemBelow);
});
};
const remove = (navigationMenuItemId: string) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
const itemToRemove = draft.find(
(item) => item.id === navigationMenuItemId,
);
if (!itemToRemove) return draft;
const isFolder = isNavigationMenuItemFolder(itemToRemove);
if (isFolder) {
return draft.filter(
(item) =>
item.id !== navigationMenuItemId &&
item.folderId !== navigationMenuItemId,
);
}
return draft.filter((item) => item.id !== navigationMenuItemId);
});
};
const moveToFolder = (
navigationMenuItemId: string,
targetFolderId: string | null,
) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
const itemToMove = draft.find((item) => item.id === navigationMenuItemId);
if (!itemToMove) return draft;
const isFolder = isNavigationMenuItemFolder(itemToMove);
if (isFolder && targetFolderId === navigationMenuItemId) {
return draft;
}
if (isFolder && isDefined(targetFolderId)) {
const descendantFolderIds = new Set<string>();
const collectDescendants = (folderId: string) => {
draft
?.filter(
(item) =>
isNavigationMenuItemFolder(item) && item.folderId === folderId,
)
.forEach((item) => {
descendantFolderIds.add(item.id);
collectDescendants(item.id);
});
};
collectDescendants(navigationMenuItemId);
if (descendantFolderIds.has(targetFolderId)) {
return draft;
}
}
const itemsInTargetFolder = draft.filter((item) =>
targetFolderId === null
? !isDefined(item.folderId)
: item.folderId === targetFolderId,
);
const maxPositionInTarget =
itemsInTargetFolder.length > 0
? Math.max(...itemsInTargetFolder.map((item) => item.position))
: -1;
const newPosition = maxPositionInTarget + 1;
return draft.map((item) =>
item.id === navigationMenuItemId
? {
...item,
folderId: targetFolderId ?? undefined,
position: newPosition,
}
: item,
);
});
};
return { moveUp, moveDown, remove, moveToFolder };
};
@@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -12,7 +13,7 @@ import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
type NavigationMenuItemFolder = {
folderId: string;
id: string;
folderName: string;
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
};
@@ -27,91 +28,114 @@ export const useNavigationMenuItemsByFolder = () => {
const views = coreViews.map(convertCoreViewToView);
const allNavigationMenuItems = [
...navigationMenuItems,
...workspaceNavigationMenuItems,
...navigationMenuItems,
];
const { folders, itemsByFolderId } = allNavigationMenuItems.reduce<{
folders: Array<{ id: string; name: string }>;
itemsByFolderId: Map<string, NavigationMenuItem[]>;
}>(
(acc, item) => {
const isFolder =
isDefined(item.name) &&
!isDefined(item.folderId) &&
!isDefined(item.targetRecordId) &&
!isDefined(item.targetObjectMetadataId) &&
!isDefined(item.viewId);
if (isFolder) {
acc.folders.push({ id: item.id, name: item.name || 'Folder' });
} else if (isDefined(item.folderId)) {
const existingItems = acc.itemsByFolderId.get(item.folderId);
if (isDefined(existingItems)) {
existingItems.push(item);
} else {
acc.itemsByFolderId.set(item.folderId, [item]);
const { workspaceFolders, userFolders, itemsByFolderId } =
allNavigationMenuItems.reduce<{
workspaceFolders: Array<{ id: string; name: string }>;
userFolders: Array<{ id: string; name: string }>;
itemsByFolderId: Map<string, NavigationMenuItem[]>;
}>(
(acc, item) => {
if (isNavigationMenuItemFolder(item)) {
const folderEntry = { id: item.id, name: item.name || 'Folder' };
if (isDefined(item.userWorkspaceId)) {
acc.userFolders.push(folderEntry);
} else {
acc.workspaceFolders.push(folderEntry);
}
} else if (isDefined(item.folderId)) {
const existingItems = acc.itemsByFolderId.get(item.folderId);
if (isDefined(existingItems)) {
existingItems.push(item);
} else {
acc.itemsByFolderId.set(item.folderId, [item]);
}
}
}
return acc;
},
{ folders: [], itemsByFolderId: new Map() },
);
const navigationMenuItemsByFolder = folders.reduce<
NavigationMenuItemFolder[]
>((acc, folder) => {
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
const targetRecordIdentifiersMap = itemsInFolder.reduce<
Map<string, ObjectRecordIdentifier>
>((map, item) => {
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
return map;
}
const targetRecordIdentifier = item.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return map;
}
const itemObjectMetadata = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (isDefined(itemObjectMetadata)) {
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier(
{
recordIdentifier: targetRecordIdentifier,
objectMetadataItem: itemObjectMetadata,
},
);
map.set(itemTargetRecordId, objectRecordIdentifier);
}
return map;
}, new Map());
const sortedItems = sortNavigationMenuItems(
itemsInFolder,
true,
views,
objectMetadataItems,
targetRecordIdentifiersMap,
return acc;
},
{
workspaceFolders: [],
userFolders: [],
itemsByFolderId: new Map(),
},
);
acc.push({
folderId: folder.id,
folderName: folder.name,
navigationMenuItems: sortedItems,
const buildFoldersList = (folders: Array<{ id: string; name: string }>) => {
const sortedFolders = [...folders].sort((a, b) => {
const folderA = allNavigationMenuItems.find((item) => item.id === a.id);
const folderB = allNavigationMenuItems.find((item) => item.id === b.id);
const positionA = folderA?.position ?? 0;
const positionB = folderB?.position ?? 0;
return positionA - positionB;
});
return acc;
}, []);
return sortedFolders.reduce<NavigationMenuItemFolder[]>((acc, folder) => {
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
return { navigationMenuItemsByFolder };
const targetRecordIdentifiersMap = itemsInFolder.reduce<
Map<string, ObjectRecordIdentifier>
>((map, item) => {
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
return map;
}
const targetRecordIdentifier = item.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return map;
}
const itemObjectMetadata = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (isDefined(itemObjectMetadata)) {
const objectRecordIdentifier =
recordIdentifierToObjectRecordIdentifier({
recordIdentifier: targetRecordIdentifier,
objectMetadataItem: itemObjectMetadata,
});
map.set(itemTargetRecordId, objectRecordIdentifier);
}
return map;
}, new Map());
const sortedItems = sortNavigationMenuItems(
itemsInFolder,
true,
views,
objectMetadataItems,
targetRecordIdentifiersMap,
);
acc.push({
id: folder.id,
folderName: folder.name,
navigationMenuItems: sortedItems,
});
return acc;
}, []);
};
const workspaceNavigationMenuItemsByFolder =
buildFoldersList(workspaceFolders);
const userNavigationMenuItemsByFolder = buildFoldersList(userFolders);
const navigationMenuItemsByFolder = [
...workspaceNavigationMenuItemsByFolder,
...userNavigationMenuItemsByFolder,
];
return {
navigationMenuItemsByFolder,
workspaceNavigationMenuItemsByFolder,
userNavigationMenuItemsByFolder,
};
};
@@ -0,0 +1,41 @@
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
export const useNavigationMenuItemsDraftState = () => {
const isNavigationMenuInEditMode = useRecoilValue(
isNavigationMenuInEditModeState,
);
const prefetchNavigationMenuItems = useRecoilValue(
prefetchNavigationMenuItemsState,
);
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const workspaceNavigationMenuItemsFromPrefetch =
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
const workspaceNavigationMenuItems =
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItemsFromPrefetch;
const isDirty =
isNavigationMenuInEditMode &&
isDefined(navigationMenuItemsDraft) &&
!isDeeplyEqual(
navigationMenuItemsDraft,
workspaceNavigationMenuItemsFromPrefetch,
);
return {
workspaceNavigationMenuItems,
isDirty,
};
};
@@ -0,0 +1,59 @@
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { coreViewsState } from '@/views/states/coreViewState';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
type NavigationMenuItemDraft = {
id?: string;
viewId?: string | null;
targetObjectMetadataId?: string | null;
};
export const useNavigationMenuObjectMetadataFromDraft = (
currentDraft: NavigationMenuItemDraft[],
) => {
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const objectMetadataIdsInWorkspace = currentDraft.reduce<Set<string>>(
(ids, item) => {
const view = isDefined(item.viewId)
? views.find((view) => view.id === item.viewId)
: undefined;
if (isDefined(view)) {
ids.add(view.objectMetadataId);
}
if (isDefined(item.targetObjectMetadataId)) {
ids.add(item.targetObjectMetadataId);
}
return ids;
},
new Set<string>(),
);
const objectMetadataIdsWithIndexView = new Set(
views
.filter((view) => view.key === ViewKey.Index)
.map((view) => view.objectMetadataId),
);
const objectMetadataIdsWithAnyView = new Set(
views.map((view) => view.objectMetadataId),
);
const viewIdsInWorkspace = new Set(
currentDraft.flatMap((item) =>
isDefined(item.viewId) ? [item.viewId] : [],
),
);
return {
views,
objectMetadataIdsInWorkspace,
objectMetadataIdsWithIndexView,
objectMetadataIdsWithAnyView,
viewIdsInWorkspace,
};
};
@@ -0,0 +1,29 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import type { IconComponent } from 'twenty-ui/display';
export const useOpenNavigationMenuItemInCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
const openNavigationMenuItemInCommandMenu = ({
pageTitle,
pageIcon,
focusTitleInput = false,
}: {
pageTitle: string;
pageIcon: IconComponent;
focusTitleInput?: boolean;
}) => {
navigateCommandMenu({
page: CommandMenuPages.NavigationMenuItemEdit,
pageTitle,
pageIcon,
resetNavigationStack: true,
focusTitleInput,
});
};
return {
openNavigationMenuItemInCommandMenu,
};
};
@@ -1,4 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -17,14 +20,24 @@ export const usePrefetchedNavigationMenuItemsData =
const prefetchNavigationMenuItems = useRecoilValue(
prefetchNavigationMenuItemsState,
);
const isNavigationMenuInEditMode = useRecoilValue(
isNavigationMenuInEditModeState,
);
const navigationMenuItemsDraft = useRecoilValue(
navigationMenuItemsDraftState,
);
const navigationMenuItems = prefetchNavigationMenuItems.filter((item) =>
isDefined(item.userWorkspaceId),
);
const workspaceNavigationMenuItems = prefetchNavigationMenuItems.filter(
(item) => !isDefined(item.userWorkspaceId),
);
const workspaceNavigationMenuItemsFromPrefetch =
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
const workspaceNavigationMenuItems =
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
? navigationMenuItemsDraft
: workspaceNavigationMenuItemsFromPrefetch;
return {
navigationMenuItems,
@@ -0,0 +1,204 @@
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
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 { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
export const useSaveNavigationMenuItemsDraft = () => {
const { updateNavigationMenuItem } = useUpdateNavigationMenuItem();
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
const [createNavigationMenuItemMutation] =
useCreateNavigationMenuItemMutation({
refetchQueries: ['FindManyNavigationMenuItems'],
});
const saveDraft = useRecoilCallback(
({ snapshot }) =>
async () => {
const draft = snapshot
.getLoadable(navigationMenuItemsDraftState)
.getValue();
const prefetch = snapshot
.getLoadable(prefetchNavigationMenuItemsState)
.getValue();
if (!draft) return;
const workspacePrefetch = filterWorkspaceNavigationMenuItems(prefetch);
const topLevelWorkspace = workspacePrefetch.filter(
(item) => !isDefined(item.folderId),
);
const draftIds = new Set(draft.map((i) => i.id));
const topLevelToDelete = topLevelWorkspace.filter(
(item) => !draftIds.has(item.id),
);
const folderIdsToDelete = new Set(
topLevelToDelete
.filter(isNavigationMenuItemFolder)
.map((item) => item.id),
);
const folderChildrenToDelete = prefetch.filter(
(item) =>
isDefined(item.folderId) && folderIdsToDelete.has(item.folderId),
);
for (const item of folderChildrenToDelete) {
await deleteNavigationMenuItem(item.id);
}
for (const item of topLevelToDelete) {
await deleteNavigationMenuItem(item.id);
}
const prefetchIds = new Set(workspacePrefetch.map((i) => i.id));
const workspacePrefetchById = new Map(
workspacePrefetch.map((i) => [i.id, i]),
);
const idsToCreate = draft.filter((item) => !prefetchIds.has(item.id));
const idsToRecreate = draft.filter((item) => {
const original = workspacePrefetchById.get(item.id);
if (!original) return false;
return (
original.viewId !== item.viewId ||
original.targetObjectMetadataId !== item.targetObjectMetadataId ||
original.targetRecordId !== item.targetRecordId
);
});
for (const draftItem of idsToRecreate) {
await deleteNavigationMenuItem(draftItem.id);
}
const idsToCreateIncludingRecreated = [
...idsToCreate,
...idsToRecreate,
];
for (const draftItem of idsToCreateIncludingRecreated) {
const input: {
position: number;
folderId?: string | null;
name?: string;
link?: string;
icon?: string | null;
viewId?: string;
targetObjectMetadataId?: string;
targetRecordId?: string;
} = {
position: Math.max(0, Math.round(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 = draftItem.folderId;
}
await createNavigationMenuItemMutation({
variables: { input },
});
}
const idsToRecreateSet = new Set(idsToRecreate.map((i) => i.id));
for (const draftItem of draft) {
if (idsToRecreateSet.has(draftItem.id)) continue;
const original = workspacePrefetchById.get(draftItem.id);
if (!original) continue;
const positionChanged = original.position !== draftItem.position;
const folderIdChanged =
(original.folderId ?? null) !== (draftItem.folderId ?? null);
const nameChanged =
(isNavigationMenuItemFolder(draftItem) ||
isNavigationMenuItemLink(draftItem)) &&
(original.name ?? null) !== (draftItem.name ?? null);
const linkChanged =
isNavigationMenuItemLink(draftItem) &&
(original.link ?? null) !== (draftItem.link ?? null);
const iconChanged =
isNavigationMenuItemFolder(draftItem) &&
(original.icon ?? null) !== (draftItem.icon ?? null);
if (
positionChanged ||
folderIdChanged ||
nameChanged ||
linkChanged ||
iconChanged
) {
const updateInput: {
id: string;
position?: number;
folderId?: string | null;
name?: string;
link?: string | null;
icon?: string | null;
} = { id: draftItem.id };
if (positionChanged) {
updateInput.position = Math.max(
0,
Math.round(draftItem.position),
);
}
if (folderIdChanged) {
updateInput.folderId = draftItem.folderId ?? null;
}
if (nameChanged && isNavigationMenuItemFolder(draftItem)) {
updateInput.name = draftItem.name ?? undefined;
}
if (nameChanged && isNavigationMenuItemLink(draftItem)) {
updateInput.name = draftItem.name ?? undefined;
}
if (linkChanged && isNavigationMenuItemLink(draftItem)) {
const linkUrl = (draftItem.link ?? '').trim();
updateInput.link = linkUrl
? linkUrl.startsWith('http://') ||
linkUrl.startsWith('https://')
? linkUrl
: `https://${linkUrl}`
: null;
}
if (iconChanged && isNavigationMenuItemFolder(draftItem)) {
updateInput.icon = draftItem.icon ?? null;
}
await updateNavigationMenuItem(updateInput);
}
}
},
[
updateNavigationMenuItem,
deleteNavigationMenuItem,
createNavigationMenuItemMutation,
],
);
return { saveDraft };
};
@@ -0,0 +1,17 @@
import { useRecoilValue } from 'recoil';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
export const useSelectedNavigationMenuItemEditItem = () => {
const selectedNavigationMenuItemInEditMode = useRecoilValue(
selectedNavigationMenuItemInEditModeState,
);
const items = useWorkspaceSectionItems();
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
: undefined;
return { selectedItem };
};
@@ -0,0 +1,19 @@
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
export const useSelectedNavigationMenuItemEditItemLabel = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const selectedItemLabel = selectedItem
? selectedItem.itemType === NavigationMenuItemType.FOLDER
? (selectedItem.name ?? 'Folder')
: selectedItem.itemType === NavigationMenuItemType.LINK
? (selectedItem.name ?? 'Link')
: (selectedItemObjectMetadata?.labelPlural ?? '')
: null;
return { selectedItemLabel };
};
@@ -0,0 +1,24 @@
import { useRecoilValue } from 'recoil';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
export const useSelectedNavigationMenuItemEditItemObjectMetadata = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const { objectMetadataItems } = useObjectMetadataItems();
const selectedItemObjectMetadata = selectedItem
? getObjectMetadataForNavigationMenuItem(
selectedItem,
objectMetadataItems,
views,
)
: null;
return { selectedItemObjectMetadata };
};
@@ -2,6 +2,8 @@ import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -76,6 +78,12 @@ export const useSortedNavigationMenuItems = () => {
const workspaceNavigationMenuItemsSorted = useMemo(() => {
const filtered = workspaceNavigationMenuItems.filter((item) => {
if (isNavigationMenuItemFolder(item)) {
return true;
}
if (isNavigationMenuItemLink(item)) {
return true;
}
if (isDefined(item.viewId)) {
return coreViews.some((view) => view.id === item.viewId);
}
@@ -90,7 +98,7 @@ export const useSortedNavigationMenuItems = () => {
});
return sortNavigationMenuItems(
filtered,
false,
true,
coreViews,
objectMetadataItems,
targetRecordIdentifiers,
@@ -0,0 +1,33 @@
import { useSetRecoilState } from 'recoil';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
export type UpdateFolderInDraftUpdates = {
name?: string;
icon?: string;
};
export const useUpdateFolderInDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const updateFolderInDraft = (
folderId: string,
updates: UpdateFolderInDraftUpdates,
) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) => {
if (!isNavigationMenuItemFolder(item) || item.id !== folderId) {
return item;
}
return { ...item, ...updates };
});
});
};
return { updateFolderInDraft };
};
@@ -0,0 +1,34 @@
import { useSetRecoilState } from 'recoil';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
export const useUpdateLinkInDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const updateLinkInDraft = (
linkId: string,
updates: { name?: string; link?: string },
) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) => {
if (item.id !== linkId || !isNavigationMenuItemLink(item)) return item;
const updated = { ...item };
if (updates.name !== undefined) {
updated.name = updates.name.trim() || 'Link';
}
if (updates.link !== undefined && updates.link.trim() !== '') {
updated.link = updates.link.trim();
}
return updated;
});
});
};
return { updateLinkInDraft };
};
@@ -0,0 +1,29 @@
import { useSetRecoilState } from 'recoil';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { type View } from '@/views/types/View';
export const useUpdateViewInDraft = () => {
const setNavigationMenuItemsDraft = useSetRecoilState(
navigationMenuItemsDraftState,
);
const updateViewInDraft = (navigationMenuItemId: string, view: View) => {
setNavigationMenuItemsDraft((draft) => {
if (!draft) return draft;
return draft.map((item) =>
item.id === navigationMenuItemId
? {
...item,
viewId: view.id,
targetObjectMetadataId: undefined,
targetRecordId: undefined,
}
: item,
);
});
};
return { updateViewInDraft };
};
@@ -1,5 +1,7 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { coreViewsState } from '@/views/states/coreViewState';
@@ -7,20 +9,38 @@ import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { isDefined } from 'twenty-shared/utils';
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
export const useWorkspaceNavigationMenuItems = (): {
workspaceNavigationMenuItemsObjectMetadataItems: ObjectMetadataItem[];
} => {
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const coreViews = useRecoilValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const workspaceFolderIds = useMemo(
() =>
new Set(
rawWorkspaceNavigationMenuItems
.filter(isNavigationMenuItemFolder)
.map((item) => item.id),
),
[rawWorkspaceNavigationMenuItems],
);
const workspaceNavigationMenuItemsIncludingFolderItems = useMemo(
() =>
rawWorkspaceNavigationMenuItems.filter(
(item) =>
!isDefined(item.folderId) ||
(isDefined(item.folderId) && workspaceFolderIds.has(item.folderId)),
),
[rawWorkspaceNavigationMenuItems, workspaceFolderIds],
);
const workspaceNavigationMenuItemViewIds = new Set(
workspaceNavigationMenuItemsSorted
workspaceNavigationMenuItemsIncludingFolderItems
.map((item) => item.viewId)
.filter((viewId) => isDefined(viewId)),
);
@@ -35,7 +55,7 @@ export const useWorkspaceNavigationMenuItems = (): {
);
const navigationMenuItemRecordObjectMetadataIds = new Set(
rawWorkspaceNavigationMenuItems
workspaceNavigationMenuItemsIncludingFolderItems
.map((item) => item.targetObjectMetadataId)
.filter((objectMetadataId) => isDefined(objectMetadataId)),
);
@@ -0,0 +1,92 @@
import { useRecoilValue } from 'recoil';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/folderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { isDefined } from 'twenty-shared/utils';
import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder';
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
export type FlatWorkspaceItem =
| ProcessedNavigationMenuItem
| (NavigationMenuItem & {
itemType: NavigationMenuItemType.FOLDER;
Icon: string;
});
export type NavigationMenuItemClickParams = {
item: FlatWorkspaceItem;
objectMetadataItem?: ObjectMetadataItem | null;
};
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
const { workspaceNavigationMenuItems } =
usePrefetchedNavigationMenuItemsData();
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { workspaceNavigationMenuItemsByFolder } =
useNavigationMenuItemsByFolder();
const coreViews = useRecoilValue(coreViewsState);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const views = coreViews.map(convertCoreViewToView);
const flatWorkspaceItems = workspaceNavigationMenuItems
.filter((item) => !isDefined(item.folderId))
.sort((a, b) => a.position - b.position);
const processedObjectViewsById = new Map(
workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]),
);
const folderChildrenById = new Map(
workspaceNavigationMenuItemsByFolder.map((folder) => [
folder.id,
folder.navigationMenuItems,
]),
);
const flatItems: FlatWorkspaceItem[] = flatWorkspaceItems.reduce<
FlatWorkspaceItem[]
>((acc, item) => {
if (isNavigationMenuItemFolder(item)) {
acc.push({
...item,
itemType: NavigationMenuItemType.FOLDER,
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
});
} else {
const processedItem = processedObjectViewsById.get(item.id);
if (!isDefined(processedItem)) {
return acc;
}
if (processedItem.itemType === NavigationMenuItemType.LINK) {
acc.push(processedItem);
} else {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
processedItem,
objectMetadataItems,
views,
);
if (isDefined(objectMetadataItem)) {
acc.push(processedItem);
}
}
}
return acc;
}, []);
return flatItems.flatMap((item) =>
item.itemType === NavigationMenuItemType.FOLDER
? [item, ...(folderChildrenById.get(item.id) ?? [])]
: [item],
);
};
@@ -0,0 +1,9 @@
import { atom } from 'recoil';
import type { AddMenuItemInsertionContext } from '@/navigation-menu-item/types/AddMenuItemInsertionContext';
export const addMenuItemInsertionContextState =
atom<AddMenuItemInsertionContext | null>({
key: 'addMenuItemInsertionContextState',
default: null,
});
@@ -0,0 +1,10 @@
import { atom } from 'recoil';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
export const addToNavPayloadRegistryState = atom<
Map<string, AddToNavigationDragPayload>
>({
key: 'navigation-menu-item/addToNavPayloadRegistryState',
default: new Map(),
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isNavigationMenuInEditModeState = atom<boolean>({
key: 'isNavigationMenuInEditModeState',
default: false,
});

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