mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 04:12:28 +00:00 
			
		
		
		
	Merge branch 'main' into c--refactor-graphql-query-runner--move-api-event-emit-before-gql-processing
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| #!/bin/sh | ||||
| set -e | ||||
|  | ||||
| # Check if the initialization has already been done and that we enabled automatic migration | ||||
| if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then | ||||
|   | ||||
| @@ -30,6 +30,7 @@ export const WorkflowRunActionEffect = () => { | ||||
|       addActionMenuEntry({ | ||||
|         type: 'workflow-run', | ||||
|         key: `workflow-run-${activeWorkflowVersion.id}`, | ||||
|         scope: 'global', | ||||
|         label: capitalize(activeWorkflowVersion.workflow.name), | ||||
|         position: index, | ||||
|         Icon: IconSettingsAutomation, | ||||
|   | ||||
| @@ -37,8 +37,8 @@ export const DeleteRecordsActionEffect = ({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const favorites = useFavorites(); | ||||
|   const deleteFavorite = useDeleteFavorite(); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|   const { deleteFavorite } = useDeleteFavorite(); | ||||
|  | ||||
|   const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( | ||||
|     contextStoreNumberOfSelectedRecordsComponentState, | ||||
| @@ -106,6 +106,7 @@ export const DeleteRecordsActionEffect = ({ | ||||
|     if (canDelete) { | ||||
|       addActionMenuEntry({ | ||||
|         type: 'standard', | ||||
|         scope: 'record-selection', | ||||
|         key: 'delete', | ||||
|         label: 'Delete', | ||||
|         position, | ||||
|   | ||||
| @@ -32,12 +32,10 @@ export const ExportRecordsActionEffect = ({ | ||||
|   useEffect(() => { | ||||
|     addActionMenuEntry({ | ||||
|       type: 'standard', | ||||
|       scope: 'record-selection', | ||||
|       key: 'export', | ||||
|       position, | ||||
|       label: displayedExportProgress( | ||||
|         contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all', | ||||
|         progress, | ||||
|       ), | ||||
|       label: displayedExportProgress(progress), | ||||
|       Icon: IconDatabaseExport, | ||||
|       accent: 'default', | ||||
|       onClick: () => download(), | ||||
|   | ||||
| @@ -23,11 +23,11 @@ export const ManageFavoritesActionEffect = ({ | ||||
|     contextStoreTargetedRecordsRuleComponentState, | ||||
|   ); | ||||
|  | ||||
|   const favorites = useFavorites(); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|  | ||||
|   const createFavorite = useCreateFavorite(); | ||||
|   const { createFavorite } = useCreateFavorite(); | ||||
|  | ||||
|   const deleteFavorite = useDeleteFavorite(); | ||||
|   const { deleteFavorite } = useDeleteFavorite(); | ||||
|  | ||||
|   const selectedRecordId = | ||||
|     contextStoreTargetedRecordsRule.mode === 'selection' | ||||
| @@ -51,6 +51,7 @@ export const ManageFavoritesActionEffect = ({ | ||||
|  | ||||
|     addActionMenuEntry({ | ||||
|       type: 'standard', | ||||
|       scope: 'record-selection', | ||||
|       key: 'manage-favorites', | ||||
|       label: isFavorite ? 'Remove from favorites' : 'Add to favorites', | ||||
|       position, | ||||
|   | ||||
| @@ -57,6 +57,7 @@ export const WorkflowRunRecordActionEffect = ({ | ||||
|       addActionMenuEntry({ | ||||
|         type: 'workflow-run', | ||||
|         key: `workflow-run-${activeWorkflowVersion.id}`, | ||||
|         scope: 'record-selection', | ||||
|         label: capitalize(activeWorkflowVersion.workflow.name), | ||||
|         position: index, | ||||
|         Icon: IconSettingsAutomation, | ||||
|   | ||||
| @@ -65,21 +65,25 @@ export const RightDrawerActionMenuDropdown = () => { | ||||
|       }} | ||||
|       dropdownComponents={ | ||||
|         <DropdownMenuItemsContainer> | ||||
|           {actionMenuEntries.map((item, index) => ( | ||||
|             <MenuItem | ||||
|               key={index} | ||||
|               LeftIcon={item.Icon} | ||||
|               onClick={() => { | ||||
|                 closeDropdown( | ||||
|                   getRightDrawerActionMenuDropdownIdFromActionMenuId( | ||||
|                     actionMenuId, | ||||
|                   ), | ||||
|                 ); | ||||
|                 item.onClick?.(); | ||||
|               }} | ||||
|               text={item.label} | ||||
|             /> | ||||
|           ))} | ||||
|           {actionMenuEntries | ||||
|             .filter( | ||||
|               (actionMenuEntry) => actionMenuEntry.scope === 'record-selection', | ||||
|             ) | ||||
|             .map((actionMenuEntry, index) => ( | ||||
|               <MenuItem | ||||
|                 key={index} | ||||
|                 LeftIcon={actionMenuEntry.Icon} | ||||
|                 onClick={() => { | ||||
|                   closeDropdown( | ||||
|                     getRightDrawerActionMenuDropdownIdFromActionMenuId( | ||||
|                       actionMenuId, | ||||
|                     ), | ||||
|                   ); | ||||
|                   actionMenuEntry.onClick?.(); | ||||
|                 }} | ||||
|                 text={actionMenuEntry.label} | ||||
|               /> | ||||
|             ))} | ||||
|         </DropdownMenuItemsContainer> | ||||
|       } | ||||
|     /> | ||||
|   | ||||
| @@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = { | ||||
|  | ||||
|             map.set('delete', { | ||||
|               isPinned: true, | ||||
|               scope: 'record-selection', | ||||
|               type: 'standard', | ||||
|               key: 'delete', | ||||
|               label: 'Delete', | ||||
|   | ||||
| @@ -22,6 +22,7 @@ export const Default: Story = { | ||||
|   args: { | ||||
|     entry: { | ||||
|       type: 'standard', | ||||
|       scope: 'record-selection', | ||||
|       key: 'delete', | ||||
|       label: 'Delete', | ||||
|       position: 0, | ||||
| @@ -35,6 +36,7 @@ export const WithDangerAccent: Story = { | ||||
|   args: { | ||||
|     entry: { | ||||
|       type: 'standard', | ||||
|       scope: 'record-selection', | ||||
|       key: 'delete', | ||||
|       label: 'Delete', | ||||
|       position: 0, | ||||
| @@ -49,6 +51,7 @@ export const WithInteraction: Story = { | ||||
|   args: { | ||||
|     entry: { | ||||
|       type: 'standard', | ||||
|       scope: 'record-selection', | ||||
|       key: 'markAsDone', | ||||
|       label: 'Mark as done', | ||||
|       position: 0, | ||||
|   | ||||
| @@ -42,6 +42,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('delete', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'delete', | ||||
|             label: 'Delete', | ||||
|             position: 0, | ||||
| @@ -51,6 +52,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('markAsDone', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'markAsDone', | ||||
|             label: 'Mark as done', | ||||
|             position: 1, | ||||
| @@ -60,6 +62,7 @@ const meta: Meta<typeof RecordIndexActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('addToFavorites', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'addToFavorites', | ||||
|             label: 'Add to favorites', | ||||
|             position: 2, | ||||
|   | ||||
| @@ -55,6 +55,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('addToFavorites', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'addToFavorites', | ||||
|             label: 'Add to favorites', | ||||
|             position: 0, | ||||
| @@ -64,6 +65,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('export', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'export', | ||||
|             label: 'Export', | ||||
|             position: 1, | ||||
| @@ -73,6 +75,7 @@ const meta: Meta<typeof RightDrawerActionMenuDropdown> = { | ||||
|  | ||||
|           map.set('delete', { | ||||
|             type: 'standard', | ||||
|             scope: 'record-selection', | ||||
|             key: 'delete', | ||||
|             label: 'Delete', | ||||
|             position: 2, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { IconComponent, MenuItemAccent } from 'twenty-ui'; | ||||
|  | ||||
| export type ActionMenuEntry = { | ||||
|   type: 'standard' | 'workflow-run'; | ||||
|   scope: 'global' | 'record-selection'; | ||||
|   key: string; | ||||
|   label: string; | ||||
|   position: number; | ||||
|   | ||||
| @@ -61,9 +61,9 @@ export const CurrentWorkspaceMemberFavorites = ({ | ||||
|   const selectedFavoriteIndex = folder.favorites.findIndex((favorite) => | ||||
|     isLocationMatchingFavorite(currentPath, currentViewPath, favorite), | ||||
|   ); | ||||
|   const handleReorderFavorite = useReorderFavorite(); | ||||
|   const { handleReorderFavorite } = useReorderFavorite(); | ||||
|  | ||||
|   const deleteFavorite = useDeleteFavorite(); | ||||
|   const { deleteFavorite } = useDeleteFavorite(); | ||||
|  | ||||
|   const favoriteFolderContentLength = folder.favorites.length; | ||||
|  | ||||
| @@ -154,6 +154,7 @@ export const CurrentWorkspaceMemberFavorites = ({ | ||||
|                     key={favorite.id} | ||||
|                     draggableId={favorite.id} | ||||
|                     index={index} | ||||
|                     isInsideScrollableContainer | ||||
|                     itemComponent={ | ||||
|                       <NavigationDrawerSubItem | ||||
|                         key={favorite.id} | ||||
|   | ||||
| @@ -4,8 +4,8 @@ import { useRecoilState, useRecoilValue } from 'recoil'; | ||||
| import { | ||||
|   IconFolderPlus, | ||||
|   IconHeartOff, | ||||
|   isDefined, | ||||
|   LightIconButton, | ||||
|   isDefined, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; | ||||
| @@ -32,9 +32,9 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => { | ||||
|   const currentViewPath = useLocation().pathname + useLocation().search; | ||||
|   const theme = useTheme(); | ||||
|   const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); | ||||
|   const favorites = useFavorites(); | ||||
|   const deleteFavorite = useDeleteFavorite(); | ||||
|   const handleReorderFavorite = useReorderFavorite(); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|   const { deleteFavorite } = useDeleteFavorite(); | ||||
|   const { handleReorderFavorite } = useReorderFavorite(); | ||||
|   const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] = | ||||
|     useRecoilState(isFavoriteFolderCreatingState); | ||||
|  | ||||
| @@ -50,24 +50,25 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => { | ||||
|   const toggleNewFolder = () => { | ||||
|     setIsFavoriteFolderCreating((current) => !current); | ||||
|   }; | ||||
|   const shouldDisplayFavoritesWithFeatureFlagEnabled = true; | ||||
|  | ||||
|   //todo: remove this logic once feature flag gating is removed | ||||
|   const shouldDisplayFavoritesWithoutFeatureFlagEnabled = | ||||
|     favorites.length > 0 || isFavoriteFolderCreating; | ||||
|  | ||||
|   const shouldDisplayFavorites = isFavoriteFolderEnabled | ||||
|     ? shouldDisplayFavoritesWithFeatureFlagEnabled | ||||
|     : shouldDisplayFavoritesWithoutFeatureFlagEnabled; | ||||
|  | ||||
|   if (loading && isDefined(currentWorkspaceMember)) { | ||||
|     return <FavoritesSkeletonLoader />; | ||||
|   } | ||||
|  | ||||
|   const currentWorkspaceMemberFavorites = favorites.filter( | ||||
|     (favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id, | ||||
|   ); | ||||
|  | ||||
|   const orphanFavorites = currentWorkspaceMemberFavorites.filter( | ||||
|   const orphanFavorites = favorites.filter( | ||||
|     (favorite) => !favorite.favoriteFolderId, | ||||
|   ); | ||||
|  | ||||
|   if ( | ||||
|     (!currentWorkspaceMemberFavorites || | ||||
|       currentWorkspaceMemberFavorites.length === 0) && | ||||
|     !isFavoriteFolderCreating | ||||
|   ) { | ||||
|   if (!shouldDisplayFavorites) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| @@ -104,6 +105,7 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => { | ||||
|                   key={favorite.id} | ||||
|                   draggableId={favorite.id} | ||||
|                   index={index} | ||||
|                   isInsideScrollableContainer={true} | ||||
|                   itemComponent={ | ||||
|                     <NavigationDrawerItem | ||||
|                       key={favorite.id} | ||||
|   | ||||
| @@ -38,6 +38,7 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({ | ||||
|       dropdownHotkeyScope={{ | ||||
|         scope: FavoriteFolderHotkeyScope.FavoriteFolderRightIconDropdown, | ||||
|       }} | ||||
|       usePortal | ||||
|       data-select-disable | ||||
|       clickableComponent={ | ||||
|         <LightIconButton Icon={IconDotsVertical} accent="tertiary" /> | ||||
|   | ||||
| @@ -18,8 +18,8 @@ export const FavoriteFolders = ({ | ||||
| }: FavoriteFoldersProps) => { | ||||
|   const [newFolderName, setNewFolderName] = useState(''); | ||||
|  | ||||
|   const favoritesByFolder = useFavoritesByFolder(); | ||||
|   const createFavoriteFolder = useCreateFavoriteFolder(); | ||||
|   const { favoritesByFolder } = useFavoritesByFolder(); | ||||
|   const { createNewFavoriteFolder } = useCreateFavoriteFolder(); | ||||
|  | ||||
|   const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] = | ||||
|     useRecoilState(isFavoriteFolderCreatingState); | ||||
| @@ -33,12 +33,12 @@ export const FavoriteFolders = ({ | ||||
|  | ||||
|     setIsFavoriteFolderCreating(false); | ||||
|     setNewFolderName(''); | ||||
|     await createFavoriteFolder(value); | ||||
|     await createNewFavoriteFolder(value); | ||||
|     return true; | ||||
|   }; | ||||
|  | ||||
|   const handleClickOutside = async ( | ||||
|     event: MouseEvent | TouchEvent, | ||||
|     _event: MouseEvent | TouchEvent, | ||||
|     value: string, | ||||
|   ) => { | ||||
|     if (!value) { | ||||
| @@ -48,7 +48,7 @@ export const FavoriteFolders = ({ | ||||
|  | ||||
|     setIsFavoriteFolderCreating(false); | ||||
|     setNewFolderName(''); | ||||
|     await createFavoriteFolder(value); | ||||
|     await createNewFavoriteFolder(value); | ||||
|   }; | ||||
|  | ||||
|   const handleCancelFavoriteFolderCreation = () => { | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { PageFavoriteButton } from '@/favorites/components/PageFavoriteButton'; | ||||
| import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker'; | ||||
| import { FavoriteFolderPickerEffect } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerEffect'; | ||||
| import { FavoriteFolderPickerScope } from '@/favorites/favorite-folder-picker/scopes/FavoriteFolderPickerScope'; | ||||
| import { FavoriteFolderPickerComponentInstanceContext } from '@/favorites/favorite-folder-picker/scopes/FavoriteFolderPickerScope'; | ||||
| import { ObjectRecord } from '@/object-record/types/ObjectRecord'; | ||||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||
| import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; | ||||
| import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton'; | ||||
| 
 | ||||
| type PageFavoriteFoldersDropdownProps = { | ||||
|   dropdownId: string; | ||||
| @@ -23,7 +23,9 @@ export const PageFavoriteFoldersDropdown = ({ | ||||
|   const { closeDropdown } = useDropdown(dropdownId); | ||||
| 
 | ||||
|   return ( | ||||
|     <FavoriteFolderPickerScope favoriteFoldersScopeId={dropdownId}> | ||||
|     <FavoriteFolderPickerComponentInstanceContext | ||||
|       favoriteFoldersScopeId={dropdownId} | ||||
|     > | ||||
|       <DropdownScope dropdownScopeId={dropdownId}> | ||||
|         <Dropdown | ||||
|           dropdownId={dropdownId} | ||||
| @@ -44,6 +46,6 @@ export const PageFavoriteFoldersDropdown = ({ | ||||
|           }} | ||||
|         /> | ||||
|       </DropdownScope> | ||||
|     </FavoriteFolderPickerScope> | ||||
|     </FavoriteFolderPickerComponentInstanceContext> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,10 +0,0 @@ | ||||
| import styled from '@emotion/styled'; | ||||
| import { MenuItemMultiSelect } from '@ui/navigation/menu-item/components/MenuItemMultiSelect'; | ||||
|  | ||||
| const StyledNoGapMenuItem = styled(MenuItemMultiSelect)` | ||||
|   & > div { | ||||
|     gap: 0; | ||||
|   } | ||||
| `; | ||||
|  | ||||
| export const FavoriteFolderMenuItemMultiSelect = StyledNoGapMenuItem; | ||||
| @@ -36,7 +36,7 @@ export const FavoriteFolderPicker = ({ | ||||
|     FavoriteFolderPickerInstanceContext, | ||||
|   ); | ||||
|  | ||||
|   const { getFoldersByIds, toggleFolderSelection } = useFavoriteFolderPicker({ | ||||
|   const { favoriteFolders, toggleFolderSelection } = useFavoriteFolderPicker({ | ||||
|     record, | ||||
|     objectNameSingular, | ||||
|   }); | ||||
| @@ -45,8 +45,7 @@ export const FavoriteFolderPicker = ({ | ||||
|     favoriteFolderSearchFilterComponentState, | ||||
|   ); | ||||
|  | ||||
|   const folders = getFoldersByIds(); | ||||
|   const filteredFolders = folders.filter((folder) => | ||||
|   const filteredFolders = favoriteFolders.filter((folder) => | ||||
|     folder.name | ||||
|       .toLowerCase() | ||||
|       .includes(favoriteFoldersSearchFilter.toLowerCase()), | ||||
| @@ -94,7 +93,7 @@ export const FavoriteFolderPicker = ({ | ||||
|       <DropdownMenuSeparator /> | ||||
|       <DropdownMenuItemsContainer hasMaxHeight> | ||||
|         <FavoriteFolderPickerList | ||||
|           folders={folders} | ||||
|           folders={favoriteFolders} | ||||
|           toggleFolderSelection={toggleFolderSelection} | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|   | ||||
| @@ -30,7 +30,7 @@ export const FavoriteFolderPickerEffect = ({ | ||||
|  | ||||
|   const { favoriteFolders } = usePrefetchedFavoritesFoldersData(); | ||||
|  | ||||
|   const favorites = useFavorites(); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|   const setCheckedState = useSetRecoilComponentStateV2( | ||||
|     favoriteFolderPickerCheckedComponentState, | ||||
|   ); | ||||
|   | ||||
| @@ -14,10 +14,6 @@ const StyledFooter = styled.div` | ||||
|   border-top: 1px solid ${({ theme }) => theme.border.color.light}; | ||||
| `; | ||||
|  | ||||
| const StyledIconPlus = styled(IconPlus)` | ||||
|   padding-left: ${({ theme }) => theme.spacing(1)}; | ||||
| `; | ||||
|  | ||||
| export const FavoriteFolderPickerFooter = () => { | ||||
|   const [, setIsFavoriteFolderCreating] = useRecoilState( | ||||
|     isFavoriteFolderCreatingState, | ||||
| @@ -35,7 +31,7 @@ export const FavoriteFolderPickerFooter = () => { | ||||
|             closeDropdown(); | ||||
|           }} | ||||
|           text="Add folder" | ||||
|           LeftIcon={() => <StyledIconPlus size={theme.icon.size.md} />} | ||||
|           LeftIcon={() => <IconPlus size={theme.icon.size.md} />} | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </StyledFooter> | ||||
|   | ||||
| @@ -4,8 +4,7 @@ import { FavoriteFolder } from '@/favorites/types/FavoriteFolder'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { MenuItem } from 'twenty-ui'; | ||||
| import { FavoriteFolderMenuItemMultiSelect } from './FavoriteFolderMenuItemMultiSelect'; | ||||
| import { MenuItem, MenuItemMultiSelect } from 'twenty-ui'; | ||||
|  | ||||
| const StyledItemsContainer = styled.div` | ||||
|   width: 100%; | ||||
| @@ -30,6 +29,7 @@ export const FavoriteFolderPickerList = ({ | ||||
|   const [favoriteFoldersSearchFilter] = useRecoilComponentStateV2( | ||||
|     favoriteFolderSearchFilterComponentState, | ||||
|   ); | ||||
|  | ||||
|   const [favoriteFolderPickerChecked] = useRecoilComponentStateV2( | ||||
|     favoriteFolderPickerCheckedComponentState, | ||||
|   ); | ||||
| @@ -47,7 +47,7 @@ export const FavoriteFolderPickerList = ({ | ||||
|   return ( | ||||
|     <StyledItemsContainer> | ||||
|       {showNoFolderOption && ( | ||||
|         <FavoriteFolderMenuItemMultiSelect | ||||
|         <MenuItemMultiSelect | ||||
|           key={`menu-${NO_FOLDER_ID}`} | ||||
|           onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)} | ||||
|           selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)} | ||||
| @@ -60,7 +60,7 @@ export const FavoriteFolderPickerList = ({ | ||||
|       )} | ||||
|       {filteredFolders.length > 0 | ||||
|         ? filteredFolders.map((folder) => ( | ||||
|             <FavoriteFolderMenuItemMultiSelect | ||||
|             <MenuItemMultiSelect | ||||
|               key={`menu-${folder.id}`} | ||||
|               onSelectChange={() => toggleFolderSelection(folder.id)} | ||||
|               selected={favoriteFolderPickerChecked.includes(folder.id)} | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState'; | ||||
| import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState'; | ||||
| import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState'; | ||||
| import { favoriteFoldersComponentSelector } from '@/favorites/favorite-folder-picker/states/selectors/favoriteFoldersComponentSelector'; | ||||
| import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite'; | ||||
| import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; | ||||
| import { useFavorites } from '@/favorites/hooks/useFavorites'; | ||||
| @@ -8,7 +7,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites'; | ||||
| import { FavoriteFolder } from '@/favorites/types/FavoriteFolder'; | ||||
| import { ObjectRecord } from '@/object-record/types/ObjectRecord'; | ||||
| import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; | ||||
| import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { useRecoilCallback } from 'recoil'; | ||||
| import { isDefined } from 'twenty-ui'; | ||||
|  | ||||
| @@ -17,46 +16,26 @@ type useFavoriteFolderPickerProps = { | ||||
|   objectNameSingular: string; | ||||
| }; | ||||
|  | ||||
| type FolderOperations = { | ||||
|   getFoldersByIds: () => FavoriteFolder[]; | ||||
| type useFavoriteFolderPickerReturnType = { | ||||
|   favoriteFolders: FavoriteFolder[]; | ||||
|   toggleFolderSelection: (folderId: string) => Promise<void>; | ||||
| }; | ||||
|  | ||||
| export const useFavoriteFolderPicker = ({ | ||||
|   record, | ||||
|   objectNameSingular, | ||||
| }: useFavoriteFolderPickerProps): FolderOperations => { | ||||
|   const [favoriteFolderIdsPicker] = useRecoilComponentStateV2( | ||||
|     favoriteFolderIdsPickerComponentState, | ||||
|   ); | ||||
|  | ||||
| }: useFavoriteFolderPickerProps): useFavoriteFolderPickerReturnType => { | ||||
|   const favoriteFoldersMultiSelectCheckedState = | ||||
|     useRecoilComponentCallbackStateV2( | ||||
|       favoriteFolderPickerCheckedComponentState, | ||||
|     ); | ||||
|  | ||||
|   const favoriteFolderPickerFamilyState = useRecoilComponentCallbackStateV2( | ||||
|     favoriteFolderPickerComponentFamilyState, | ||||
|   ); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|   const { createFavorite } = useCreateFavorite(); | ||||
|   const { deleteFavorite } = useDeleteFavorite(); | ||||
|  | ||||
|   const favorites = useFavorites(); | ||||
|   const createFavorite = useCreateFavorite(); | ||||
|   const deleteFavorite = useDeleteFavorite(); | ||||
|  | ||||
|   const getFoldersByIds = useRecoilCallback( | ||||
|     ({ snapshot }) => | ||||
|       (): FavoriteFolder[] => { | ||||
|         return favoriteFolderIdsPicker | ||||
|           .map((folderId) => { | ||||
|             const folderValue = snapshot | ||||
|               .getLoadable(favoriteFolderPickerFamilyState(folderId)) | ||||
|               .getValue(); | ||||
|  | ||||
|             return folderValue; | ||||
|           }) | ||||
|           .filter((folder): folder is FavoriteFolder => isDefined(folder)); | ||||
|       }, | ||||
|     [favoriteFolderIdsPicker, favoriteFolderPickerFamilyState], | ||||
|   const favoriteFolders = useRecoilComponentValueV2( | ||||
|     favoriteFoldersComponentSelector, | ||||
|   ); | ||||
|  | ||||
|   const toggleFolderSelection = useRecoilCallback( | ||||
| @@ -123,7 +102,7 @@ export const useFavoriteFolderPicker = ({ | ||||
|   ); | ||||
|  | ||||
|   return { | ||||
|     getFoldersByIds, | ||||
|     favoriteFolders, | ||||
|     toggleFolderSelection, | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext'; | ||||
| import { ReactNode } from 'react'; | ||||
|  | ||||
| type FavoriteFolderPickerScopeProps = { | ||||
| type FavoriteFolderPickerComponentInstanceContextProps = { | ||||
|   children: ReactNode; | ||||
|   favoriteFoldersScopeId: string; | ||||
| }; | ||||
|  | ||||
| export const FavoriteFolderPickerScope = ({ | ||||
| export const FavoriteFolderPickerComponentInstanceContext = ({ | ||||
|   children, | ||||
|   favoriteFoldersScopeId, | ||||
| }: FavoriteFolderPickerScopeProps) => { | ||||
| }: FavoriteFolderPickerComponentInstanceContextProps) => { | ||||
|   return ( | ||||
|     <FavoriteFolderPickerInstanceContext.Provider | ||||
|       value={{ instanceId: favoriteFoldersScopeId }} | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext'; | ||||
| import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; | ||||
|  | ||||
| export const favoriteFolderLoadingComponentState = | ||||
|   createComponentStateV2<boolean>({ | ||||
|     key: 'favoriteFoldersLoadingComponentState', | ||||
|     defaultValue: false, | ||||
|     componentInstanceContext: FavoriteFolderPickerInstanceContext, | ||||
|   }); | ||||
| @@ -0,0 +1,31 @@ | ||||
| import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext'; | ||||
| import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState'; | ||||
| import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState'; | ||||
| import { FavoriteFolder } from '@/favorites/types/FavoriteFolder'; | ||||
| import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; | ||||
| import { isDefined } from 'twenty-ui'; | ||||
|  | ||||
| export const favoriteFoldersComponentSelector = createComponentSelectorV2< | ||||
|   FavoriteFolder[] | ||||
| >({ | ||||
|   key: 'favoriteFoldersComponentSelector', | ||||
|   componentInstanceContext: FavoriteFolderPickerInstanceContext, | ||||
|   get: | ||||
|     ({ instanceId }) => | ||||
|     ({ get }) => { | ||||
|       const folderIds = get( | ||||
|         favoriteFolderIdsPickerComponentState.atomFamily({ instanceId }), | ||||
|       ); | ||||
|  | ||||
|       return folderIds | ||||
|         .map((folderId: string) => | ||||
|           get( | ||||
|             favoriteFolderPickerComponentFamilyState.atomFamily({ | ||||
|               instanceId, | ||||
|               familyKey: folderId, | ||||
|             }), | ||||
|           ), | ||||
|         ) | ||||
|         .filter((folder): folder is FavoriteFolder => isDefined(folder)); | ||||
|     }, | ||||
| }); | ||||
| @@ -44,7 +44,10 @@ describe('useCreateFavorite', () => { | ||||
|       { wrapper: Wrapper }, | ||||
|     ); | ||||
|  | ||||
|     result.current(favoriteTargetObjectRecord, CoreObjectNameSingular.Person); | ||||
|     result.current.createFavorite( | ||||
|       favoriteTargetObjectRecord, | ||||
|       CoreObjectNameSingular.Person, | ||||
|     ); | ||||
|  | ||||
|     await waitFor(() => { | ||||
|       expect(mocks[0].result).toHaveBeenCalled(); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ describe('useDeleteFavorite', () => { | ||||
|       { wrapper: Wrapper }, | ||||
|     ); | ||||
|  | ||||
|     result.current(favoriteId); | ||||
|     result.current.deleteFavorite(favoriteId); | ||||
|  | ||||
|     await waitFor(() => { | ||||
|       expect(mocks[1].result).toHaveBeenCalled(); | ||||
|   | ||||
| @@ -38,6 +38,6 @@ describe('useFavorites', () => { | ||||
|       { wrapper: Wrapper }, | ||||
|     ); | ||||
|  | ||||
|     expect(result.current).toEqual(sortedFavorites); | ||||
|     expect(result.current.sortedFavorites).toEqual(sortedFavorites); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -54,7 +54,10 @@ describe('useReorderFavorite', () => { | ||||
|         announce: () => {}, | ||||
|       }; | ||||
|  | ||||
|       result.current(dragAndDropResult, responderProvided); | ||||
|       result.current.handleReorderFavorite( | ||||
|         dragAndDropResult, | ||||
|         responderProvided, | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     await waitFor(() => { | ||||
|   | ||||
| @@ -34,5 +34,5 @@ export const useCreateFavorite = () => { | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return createFavorite; | ||||
|   return { createFavorite }; | ||||
| }; | ||||
|   | ||||
| @@ -28,5 +28,5 @@ export const useCreateFavoriteFolder = () => { | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return createNewFavoriteFolder; | ||||
|   return { createNewFavoriteFolder }; | ||||
| }; | ||||
|   | ||||
| @@ -10,5 +10,5 @@ export const useDeleteFavorite = () => { | ||||
|     deleteOneRecord(favoriteId); | ||||
|   }; | ||||
|  | ||||
|   return deleteFavorite; | ||||
|   return { deleteFavorite }; | ||||
| }; | ||||
|   | ||||
| @@ -1,23 +1,46 @@ | ||||
| import { Favorite } from '@/favorites/types/Favorite'; | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||
| import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; | ||||
| import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; | ||||
| import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData'; | ||||
| import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; | ||||
| import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery'; | ||||
| import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; | ||||
|  | ||||
| export const useDeleteFavoriteFolder = () => { | ||||
|   const { deleteOneRecord } = useDeleteOneRecord({ | ||||
|     objectNameSingular: CoreObjectNameSingular.FavoriteFolder, | ||||
|   }); | ||||
|   const { upsertFavorites, favorites, workspaceFavorites } = | ||||
|     usePrefetchedFavoritesData(); | ||||
|  | ||||
|   const { upsertRecordsInCache } = usePrefetchRunQuery<Favorite>({ | ||||
|     prefetchKey: PrefetchKey.AllFavorites, | ||||
|   }); | ||||
|  | ||||
|   const { objectMetadataItem } = useObjectMetadataItem({ | ||||
|     objectNameSingular: | ||||
|       PREFETCH_CONFIG[PrefetchKey.AllFavorites].objectNameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { readFindManyRecordsQueryInCache } = | ||||
|     useReadFindManyRecordsQueryInCache({ | ||||
|       objectMetadataItem, | ||||
|     }); | ||||
|  | ||||
|   const deleteFavoriteFolder = async (folderId: string): Promise<void> => { | ||||
|     await deleteOneRecord(folderId); | ||||
|  | ||||
|     const updatedFavorites = [ | ||||
|       ...favorites.filter((favorite) => favorite.favoriteFolderId !== folderId), | ||||
|       ...workspaceFavorites, | ||||
|     ]; | ||||
|     const allFavorites = readFindManyRecordsQueryInCache<Favorite>({ | ||||
|       queryVariables: {}, | ||||
|       recordGqlFields: PREFETCH_CONFIG[ | ||||
|         PrefetchKey.AllFavorites | ||||
|       ].operationSignatureFactory({ objectMetadataItem }).fields, | ||||
|     }); | ||||
|  | ||||
|     upsertFavorites(updatedFavorites); | ||||
|     const updatedFavorites = allFavorites.filter( | ||||
|       (favorite) => favorite.favoriteFolderId !== folderId, | ||||
|     ); | ||||
|  | ||||
|     upsertRecordsInCache(updatedFavorites); | ||||
|   }; | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -52,5 +52,5 @@ export const useFavorites = () => { | ||||
|     ], | ||||
|   ); | ||||
|  | ||||
|   return sortedFavorites; | ||||
|   return { sortedFavorites }; | ||||
| }; | ||||
|   | ||||
| @@ -1,62 +1,30 @@ | ||||
| import { sortFavorites } from '@/favorites/utils/sortFavorites'; | ||||
| import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||
| import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; | ||||
| import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; | ||||
| import { View } from '@/views/types/View'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { useFavoritesMetadata } from './useFavoritesMetadata'; | ||||
| import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData'; | ||||
| import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData'; | ||||
|  | ||||
| export const useFavoritesByFolder = () => { | ||||
|   const { favorites } = usePrefetchedFavoritesData(); | ||||
|   const { favoriteFolders } = usePrefetchedFavoritesFoldersData(); | ||||
|   const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); | ||||
|   const objectMetadataItems = useRecoilValue(objectMetadataItemsState); | ||||
|   const getObjectRecordIdentifierByNameSingular = | ||||
|     useGetObjectRecordIdentifierByNameSingular(); | ||||
|  | ||||
|   const { objectMetadataItem: favoriteObjectMetadataItem } = | ||||
|     useObjectMetadataItem({ | ||||
|       objectNameSingular: CoreObjectNameSingular.Favorite, | ||||
|     }); | ||||
|  | ||||
|   const favoriteRelationFields = useMemo( | ||||
|     () => | ||||
|       favoriteObjectMetadataItem.fields.filter( | ||||
|         (fieldMetadataItem) => | ||||
|           fieldMetadataItem.type === FieldMetadataType.Relation && | ||||
|           fieldMetadataItem.name !== 'workspaceMember' && | ||||
|           fieldMetadataItem.name !== 'favoriteFolder', | ||||
|       ), | ||||
|     [favoriteObjectMetadataItem.fields], | ||||
|   ); | ||||
|  | ||||
|   const favoritesByFolder = useMemo(() => { | ||||
|     return favoriteFolders.map((folder) => ({ | ||||
|       folderId: folder.id, | ||||
|       folderName: folder.name, | ||||
|       favorites: sortFavorites( | ||||
|         favorites.filter((favorite) => favorite.favoriteFolderId === folder.id), | ||||
|         favoriteRelationFields, | ||||
|         getObjectRecordIdentifierByNameSingular, | ||||
|         true, | ||||
|         views, | ||||
|         objectMetadataItems, | ||||
|       ), | ||||
|     })); | ||||
|   }, [ | ||||
|     favoriteFolders, | ||||
|     favorites, | ||||
|     favoriteRelationFields, | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|   const { | ||||
|     views, | ||||
|     objectMetadataItems, | ||||
|   ]); | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|     favoriteRelationFields, | ||||
|   } = useFavoritesMetadata(); | ||||
|  | ||||
|   return favoritesByFolder; | ||||
|   const favoritesByFolder = favoriteFolders.map((folder) => ({ | ||||
|     folderId: folder.id, | ||||
|     folderName: folder.name, | ||||
|     favorites: sortFavorites( | ||||
|       favorites.filter((favorite) => favorite.favoriteFolderId === folder.id), | ||||
|       favoriteRelationFields, | ||||
|       getObjectRecordIdentifierByNameSingular, | ||||
|       true, | ||||
|       views, | ||||
|       objectMetadataItems, | ||||
|     ), | ||||
|   })); | ||||
|  | ||||
|   return { favoritesByFolder }; | ||||
| }; | ||||
|   | ||||
| @@ -0,0 +1,35 @@ | ||||
| import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||
| import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; | ||||
| import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; | ||||
| import { View } from '@/views/types/View'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
|  | ||||
| export const useFavoritesMetadata = () => { | ||||
|   const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); | ||||
|   const objectMetadataItems = useRecoilValue(objectMetadataItemsState); | ||||
|   const getObjectRecordIdentifierByNameSingular = | ||||
|     useGetObjectRecordIdentifierByNameSingular(); | ||||
|  | ||||
|   const { objectMetadataItem: favoriteObjectMetadataItem } = | ||||
|     useObjectMetadataItem({ | ||||
|       objectNameSingular: CoreObjectNameSingular.Favorite, | ||||
|     }); | ||||
|  | ||||
|   const favoriteRelationFields = favoriteObjectMetadataItem.fields.filter( | ||||
|     (fieldMetadataItem) => | ||||
|       fieldMetadataItem.type === FieldMetadataType.Relation && | ||||
|       fieldMetadataItem.name !== 'workspaceMember' && | ||||
|       fieldMetadataItem.name !== 'favoriteFolder', | ||||
|   ); | ||||
|  | ||||
|   return { | ||||
|     views, | ||||
|     objectMetadataItems, | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|     favoriteRelationFields, | ||||
|   }; | ||||
| }; | ||||
| @@ -12,7 +12,7 @@ export const useReorderFavorite = () => { | ||||
|     objectNameSingular: CoreObjectNameSingular.Favorite, | ||||
|   }); | ||||
|  | ||||
|   const reorderFavorite: OnDragEndResponder = (result) => { | ||||
|   const handleReorderFavorite: OnDragEndResponder = (result) => { | ||||
|     if (!result.destination) return; | ||||
|  | ||||
|     const draggedFavoriteId = result.draggableId; | ||||
| @@ -37,5 +37,5 @@ export const useReorderFavorite = () => { | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return reorderFavorite; | ||||
|   return { handleReorderFavorite }; | ||||
| }; | ||||
|   | ||||
| @@ -1,50 +1,28 @@ | ||||
| import { sortFavorites } from '@/favorites/utils/sortFavorites'; | ||||
| import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||
| import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; | ||||
| import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; | ||||
| import { View } from '@/views/types/View'; | ||||
| import { useMemo } from 'react'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { useFavoritesMetadata } from './useFavoritesMetadata'; | ||||
| import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData'; | ||||
|  | ||||
| export const useSortedFavorites = () => { | ||||
|   const { favorites, workspaceFavorites } = usePrefetchedFavoritesData(); | ||||
|   const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); | ||||
|   const objectMetadataItems = useRecoilValue(objectMetadataItemsState); | ||||
|   const { objectMetadataItem: favoriteObjectMetadataItem } = | ||||
|     useObjectMetadataItem({ | ||||
|       objectNameSingular: CoreObjectNameSingular.Favorite, | ||||
|     }); | ||||
|  | ||||
|   const getObjectRecordIdentifierByNameSingular = | ||||
|     useGetObjectRecordIdentifierByNameSingular(); | ||||
|  | ||||
|   const favoriteRelationFieldMetadataItems = useMemo( | ||||
|     () => | ||||
|       favoriteObjectMetadataItem.fields.filter( | ||||
|         (fieldMetadataItem) => | ||||
|           fieldMetadataItem.type === FieldMetadataType.Relation && | ||||
|           fieldMetadataItem.name !== 'workspaceMember' && | ||||
|           fieldMetadataItem.name !== 'favoriteFolder', | ||||
|       ), | ||||
|     [favoriteObjectMetadataItem.fields], | ||||
|   ); | ||||
|   const { | ||||
|     views, | ||||
|     objectMetadataItems, | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|     favoriteRelationFields, | ||||
|   } = useFavoritesMetadata(); | ||||
|  | ||||
|   const favoritesSorted = useMemo(() => { | ||||
|     return sortFavorites( | ||||
|       favorites, | ||||
|       favoriteRelationFieldMetadataItems, | ||||
|       favoriteRelationFields, | ||||
|       getObjectRecordIdentifierByNameSingular, | ||||
|       true, | ||||
|       views, | ||||
|       objectMetadataItems, | ||||
|     ); | ||||
|   }, [ | ||||
|     favoriteRelationFieldMetadataItems, | ||||
|     favoriteRelationFields, | ||||
|     favorites, | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|     views, | ||||
| @@ -54,14 +32,14 @@ export const useSortedFavorites = () => { | ||||
|   const workspaceFavoritesSorted = useMemo(() => { | ||||
|     return sortFavorites( | ||||
|       workspaceFavorites.filter((favorite) => favorite.viewId), | ||||
|       favoriteRelationFieldMetadataItems, | ||||
|       favoriteRelationFields, | ||||
|       getObjectRecordIdentifierByNameSingular, | ||||
|       false, | ||||
|       views, | ||||
|       objectMetadataItems, | ||||
|     ); | ||||
|   }, [ | ||||
|     favoriteRelationFieldMetadataItems, | ||||
|     favoriteRelationFields, | ||||
|     getObjectRecordIdentifierByNameSingular, | ||||
|     workspaceFavorites, | ||||
|     views, | ||||
|   | ||||
| @@ -52,5 +52,5 @@ export const useWorkspaceFavorites = () => { | ||||
|     ], | ||||
|   ); | ||||
|  | ||||
|   return sortedWorkspaceFavorites; | ||||
|   return { sortedWorkspaceFavorites }; | ||||
| }; | ||||
|   | ||||
| @@ -6,23 +6,20 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; | ||||
| import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders'; | ||||
| import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites'; | ||||
| import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection'; | ||||
| import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper'; | ||||
| import { RemoteNavigationDrawerSection } from '@/object-metadata/components/RemoteNavigationDrawerSection'; | ||||
| import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; | ||||
| import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; | ||||
| import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; | ||||
| import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; | ||||
| import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
| import styled from '@emotion/styled'; | ||||
|  | ||||
| const StyledMainSection = styled(NavigationDrawerSection)` | ||||
|   min-height: fit-content; | ||||
| `; | ||||
|  | ||||
| const StyledContainer = styled.div` | ||||
|   overflow-x: hidden; | ||||
|   overflow-y: auto; | ||||
| `; | ||||
| export const MainNavigationDrawerItems = () => { | ||||
|   const isMobile = useIsMobile(); | ||||
|   const { toggleCommandMenu } = useCommandMenu(); | ||||
| @@ -59,15 +56,16 @@ export const MainNavigationDrawerItems = () => { | ||||
|           /> | ||||
|         </StyledMainSection> | ||||
|       )} | ||||
|       <StyledContainer> | ||||
|       <ScrollWrapper | ||||
|         contextProviderName="navigationDrawer" | ||||
|         enableXScroll={false} | ||||
|         scrollHide={true} | ||||
|       > | ||||
|         <NavigationDrawerOpenedSection /> | ||||
|  | ||||
|         <CurrentWorkspaceMemberFavoritesFolders /> | ||||
|  | ||||
|         <WorkspaceFavorites /> | ||||
|  | ||||
|         <NavigationDrawerSectionForObjectMetadataItemsWrapper isRemote={true} /> | ||||
|       </StyledContainer> | ||||
|         <RemoteNavigationDrawerSection /> | ||||
|       </ScrollWrapper> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -7,7 +7,8 @@ import { View } from '@/views/types/View'; | ||||
| export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => { | ||||
|   const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); | ||||
|  | ||||
|   const workspaceFavorites = useWorkspaceFavorites(); | ||||
|   const { sortedWorkspaceFavorites: workspaceFavorites } = | ||||
|     useWorkspaceFavorites(); | ||||
|  | ||||
|   const workspaceFavoriteIds = new Set( | ||||
|     workspaceFavorites.map((favorite) => favorite.recordId), | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat | ||||
| import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; | ||||
| import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; | ||||
| import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; | ||||
| import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
| import styled from '@emotion/styled'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
|  | ||||
| @@ -78,19 +77,15 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ | ||||
|             onClick={() => toggleNavigationSection()} | ||||
|           /> | ||||
|         </NavigationDrawerAnimatedCollapseWrapper> | ||||
|         <ScrollWrapper contextProviderName="navigationDrawer"> | ||||
|           <StyledObjectsMetaDataItemsWrapper> | ||||
|             {isNavigationSectionOpen && | ||||
|               objectMetadataItemsForNavigationItems.map( | ||||
|                 (objectMetadataItem) => ( | ||||
|                   <NavigationDrawerItemForObjectMetadataItem | ||||
|                     key={`navigation-drawer-item-${objectMetadataItem.id}`} | ||||
|                     objectMetadataItem={objectMetadataItem} | ||||
|                   /> | ||||
|                 ), | ||||
|               )} | ||||
|           </StyledObjectsMetaDataItemsWrapper> | ||||
|         </ScrollWrapper> | ||||
|         <StyledObjectsMetaDataItemsWrapper> | ||||
|           {isNavigationSectionOpen && | ||||
|             objectMetadataItemsForNavigationItems.map((objectMetadataItem) => ( | ||||
|               <NavigationDrawerItemForObjectMetadataItem | ||||
|                 key={`navigation-drawer-item-${objectMetadataItem.id}`} | ||||
|                 objectMetadataItem={objectMetadataItem} | ||||
|               /> | ||||
|             ))} | ||||
|         </StyledObjectsMetaDataItemsWrapper> | ||||
|       </NavigationDrawerSection> | ||||
|     ) | ||||
|   ); | ||||
|   | ||||
| @@ -7,16 +7,12 @@ import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/o | ||||
| import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; | ||||
| import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; | ||||
| 
 | ||||
| export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ | ||||
|   isRemote, | ||||
| }: { | ||||
|   isRemote: boolean; | ||||
| }) => { | ||||
| export const RemoteNavigationDrawerSection = () => { | ||||
|   const currentUser = useRecoilValue(currentUserState); | ||||
| 
 | ||||
|   const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); | ||||
|   const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( | ||||
|     (item) => (isRemote ? item.isRemote : !item.isRemote), | ||||
|     (item) => item.isRemote, | ||||
|   ); | ||||
|   const loading = useIsPrefetchLoading(); | ||||
| 
 | ||||
| @@ -26,9 +22,9 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ | ||||
| 
 | ||||
|   return ( | ||||
|     <NavigationDrawerSectionForObjectMetadataItems | ||||
|       sectionTitle={isRemote ? 'Remote' : 'Workspace'} | ||||
|       sectionTitle={'Remote'} | ||||
|       objectMetadataItems={filteredActiveObjectMetadataItems} | ||||
|       isRemote={isRemote} | ||||
|       isRemote={true} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,44 +0,0 @@ | ||||
| import { Meta, StoryObj } from '@storybook/react'; | ||||
|  | ||||
| import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; | ||||
| import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; | ||||
| import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; | ||||
| import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; | ||||
| import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; | ||||
| import { graphqlMocks } from '~/testing/graphqlMocks'; | ||||
|  | ||||
| import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper'; | ||||
| import { within } from '@storybook/test'; | ||||
| import { PrefetchLoadedDecorator } from '~/testing/decorators/PrefetchLoadedDecorator'; | ||||
|  | ||||
| const meta: Meta<typeof NavigationDrawerSectionForObjectMetadataItemsWrapper> = | ||||
|   { | ||||
|     title: | ||||
|       'Modules/ObjectMetadata/NavigationDrawerSectionForObjectMetadataItemsWrapper', | ||||
|     component: NavigationDrawerSectionForObjectMetadataItemsWrapper, | ||||
|     decorators: [ | ||||
|       IconsProviderDecorator, | ||||
|       ObjectMetadataItemsDecorator, | ||||
|       ComponentWithRouterDecorator, | ||||
|       ComponentWithRecoilScopeDecorator, | ||||
|       SnackBarDecorator, | ||||
|       PrefetchLoadedDecorator, | ||||
|     ], | ||||
|     parameters: { | ||||
|       msw: graphqlMocks, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
| export default meta; | ||||
| type Story = StoryObj< | ||||
|   typeof NavigationDrawerSectionForObjectMetadataItemsWrapper | ||||
| >; | ||||
|  | ||||
| export const Default: Story = { | ||||
|   play: async ({ canvasElement }) => { | ||||
|     const canvas = within(canvasElement); | ||||
|     await canvas.findByText('People', undefined, { timeout: 10000 }); | ||||
|     await canvas.findByText('Companies'); | ||||
|     await canvas.findByText('Opportunities'); | ||||
|   }, | ||||
| }; | ||||
| @@ -109,6 +109,7 @@ export type PhonesFilter = { | ||||
| export type SelectFilter = { | ||||
|   is?: IsFilter; | ||||
|   in?: string[]; | ||||
|   eq?: string; | ||||
| }; | ||||
|  | ||||
| export type MultiSelectFilter = { | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil'; | ||||
| import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||||
| import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; | ||||
| import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; | ||||
| import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; | ||||
| import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; | ||||
| import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable'; | ||||
| import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; | ||||
| @@ -68,7 +67,7 @@ export const useGenerateCombinedSearchRecordsQuery = ({ | ||||
|     ) { | ||||
|       ${filteredQueryKeyWithObjectMetadataItemArray | ||||
|         .map( | ||||
|           ({ objectMetadataItem, fields }) => | ||||
|           ({ objectMetadataItem }) => | ||||
|             `${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize( | ||||
|               objectMetadataItem.nameSingular, | ||||
|             )}, | ||||
| @@ -79,11 +78,6 @@ export const useGenerateCombinedSearchRecordsQuery = ({ | ||||
|             node ${mapObjectMetadataToGraphQLQuery({ | ||||
|               objectMetadataItems: objectMetadataItems, | ||||
|               objectMetadataItem, | ||||
|               recordGqlFields: | ||||
|                 fields ?? | ||||
|                 generateDepthOneRecordGqlFields({ | ||||
|                   objectMetadataItem, | ||||
|                 }), | ||||
|             })} | ||||
|             cursor | ||||
|           } | ||||
|   | ||||
| @@ -0,0 +1,57 @@ | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { ObjectOptionsDropdownButton } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownButton'; | ||||
| import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent'; | ||||
| import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; | ||||
| import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; | ||||
| import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; | ||||
| import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; | ||||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { useCallback, useState } from 'react'; | ||||
|  | ||||
| type ObjectOptionsDropdownProps = { | ||||
|   viewType: ViewType; | ||||
|   objectMetadataItem: ObjectMetadataItem; | ||||
|   recordIndexId: string; | ||||
| }; | ||||
|  | ||||
| export const ObjectOptionsDropdown = ({ | ||||
|   recordIndexId, | ||||
|   objectMetadataItem, | ||||
|   viewType, | ||||
| }: ObjectOptionsDropdownProps) => { | ||||
|   const [currentContentId, setCurrentContentId] = | ||||
|     useState<ObjectOptionsContentId | null>(null); | ||||
|  | ||||
|   const handleContentChange = useCallback((key: ObjectOptionsContentId) => { | ||||
|     setCurrentContentId(key); | ||||
|   }, []); | ||||
|  | ||||
|   const handleResetContent = useCallback(() => { | ||||
|     setCurrentContentId(null); | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <Dropdown | ||||
|       dropdownId={OBJECT_OPTIONS_DROPDOWN_ID} | ||||
|       clickableComponent={<ObjectOptionsDropdownButton />} | ||||
|       dropdownMenuWidth={'200px'} | ||||
|       dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }} | ||||
|       dropdownOffset={{ y: 8 }} | ||||
|       dropdownComponents={ | ||||
|         <ObjectOptionsDropdownContext.Provider | ||||
|           value={{ | ||||
|             viewType, | ||||
|             objectMetadataItem, | ||||
|             recordIndexId, | ||||
|             currentContentId, | ||||
|             onContentChange: handleContentChange, | ||||
|             resetContent: handleResetContent, | ||||
|           }} | ||||
|         > | ||||
|           <ObjectOptionsDropdownContent /> | ||||
|         </ObjectOptionsDropdownContext.Provider> | ||||
|       } | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; | ||||
| import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; | ||||
| import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; | ||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||
| 
 | ||||
| export const RecordIndexOptionsDropdownButton = () => { | ||||
| export const ObjectOptionsDropdownButton = () => { | ||||
|   const { isDropdownOpen, toggleDropdown } = useDropdown( | ||||
|     RECORD_INDEX_OPTIONS_DROPDOWN_ID, | ||||
|     OBJECT_OPTIONS_DROPDOWN_ID, | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
| @@ -0,0 +1,32 @@ | ||||
| import { ObjectOptionsDropdownFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent'; | ||||
| import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent'; | ||||
| import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent'; | ||||
| import { ObjectOptionsDropdownMenuContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent'; | ||||
| import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent'; | ||||
| import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent'; | ||||
| import { ObjectOptionsDropdownRecordGroupSortContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent'; | ||||
| import { ObjectOptionsDropdownViewSettingsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownViewSettingsContent'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
|  | ||||
| export const ObjectOptionsDropdownContent = () => { | ||||
|   const { currentContentId } = useOptionsDropdown(); | ||||
|  | ||||
|   switch (currentContentId) { | ||||
|     case 'viewSettings': | ||||
|       return <ObjectOptionsDropdownViewSettingsContent />; | ||||
|     case 'fields': | ||||
|       return <ObjectOptionsDropdownFieldsContent />; | ||||
|     case 'hiddenFields': | ||||
|       return <ObjectOptionsDropdownHiddenFieldsContent />; | ||||
|     case 'recordGroups': | ||||
|       return <ObjectOptionsDropdownRecordGroupsContent />; | ||||
|     case 'recordGroupFields': | ||||
|       return <ObjectOptionsDropdownRecordGroupFieldsContent />; | ||||
|     case 'recordGroupSort': | ||||
|       return <ObjectOptionsDropdownRecordGroupSortContent />; | ||||
|     case 'hiddenRecordGroups': | ||||
|       return <ObjectOptionsDropdownHiddenRecordGroupsContent />; | ||||
|     default: | ||||
|       return <ObjectOptionsDropdownMenuContent />; | ||||
|   } | ||||
| }; | ||||
| @@ -0,0 +1,77 @@ | ||||
| import { IconChevronLeft, IconEyeOff, MenuItemNavigate } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
| import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
|  | ||||
| export const ObjectOptionsDropdownFieldsContent = () => { | ||||
|   const { | ||||
|     viewType, | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     resetContent, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { | ||||
|     handleColumnVisibilityChange, | ||||
|     handleReorderColumns, | ||||
|     visibleTableColumns, | ||||
|   } = useObjectOptionsForTable(recordIndexId); | ||||
|  | ||||
|   const { | ||||
|     visibleBoardFields, | ||||
|     handleReorderBoardFields, | ||||
|     handleBoardFieldVisibilityChange, | ||||
|   } = useObjectOptionsForBoard({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|     recordBoardId: recordIndexId, | ||||
|     viewBarId: recordIndexId, | ||||
|   }); | ||||
|  | ||||
|   const visibleRecordFields = | ||||
|     viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns; | ||||
|  | ||||
|   const handleReorderFields = | ||||
|     viewType === ViewType.Kanban | ||||
|       ? handleReorderBoardFields | ||||
|       : handleReorderColumns; | ||||
|  | ||||
|   const handleChangeFieldVisibility = | ||||
|     viewType === ViewType.Kanban | ||||
|       ? handleBoardFieldVisibilityChange | ||||
|       : handleColumnVisibilityChange; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}> | ||||
|         Fields | ||||
|       </DropdownMenuHeader> | ||||
|       <ScrollWrapper contextProviderName="dropdownMenuItemsContainer"> | ||||
|         <ViewFieldsVisibilityDropdownSection | ||||
|           title="Visible" | ||||
|           fields={visibleRecordFields} | ||||
|           isDraggable | ||||
|           onDragEnd={handleReorderFields} | ||||
|           onVisibilityChange={handleChangeFieldVisibility} | ||||
|           showSubheader={false} | ||||
|           showDragGrip={true} | ||||
|         /> | ||||
|       </ScrollWrapper> | ||||
|       <DropdownMenuSeparator /> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <MenuItemNavigate | ||||
|           onClick={() => onContentChange('hiddenFields')} | ||||
|           LeftIcon={IconEyeOff} | ||||
|           text="Hidden Fields" | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,100 @@ | ||||
| import { useLocation } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
| import { | ||||
|   IconChevronLeft, | ||||
|   IconSettings, | ||||
|   MenuItem, | ||||
|   UndecoratedLink, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; | ||||
|  | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | ||||
| import { SettingsPath } from '@/types/SettingsPath'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
| import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
|  | ||||
| export const ObjectOptionsDropdownHiddenFieldsContent = () => { | ||||
|   const { | ||||
|     viewType, | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     closeDropdown, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { objectNamePlural } = useObjectNamePluralFromSingular({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { | ||||
|     objectSlug: objectNamePlural, | ||||
|   }); | ||||
|  | ||||
|   const { handleColumnVisibilityChange, hiddenTableColumns } = | ||||
|     useObjectOptionsForTable(recordIndexId); | ||||
|  | ||||
|   const { hiddenBoardFields, handleBoardFieldVisibilityChange } = | ||||
|     useObjectOptionsForBoard({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       recordBoardId: recordIndexId, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   const hiddenRecordFields = | ||||
|     viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns; | ||||
|  | ||||
|   const handleChangeFieldVisibility = | ||||
|     viewType === ViewType.Kanban | ||||
|       ? handleBoardFieldVisibilityChange | ||||
|       : handleColumnVisibilityChange; | ||||
|  | ||||
|   const location = useLocation(); | ||||
|   const setNavigationMemorizedUrl = useSetRecoilState( | ||||
|     navigationMemorizedUrlState, | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader | ||||
|         StartIcon={IconChevronLeft} | ||||
|         onClick={() => onContentChange('fields')} | ||||
|       > | ||||
|         Hidden Fields | ||||
|       </DropdownMenuHeader> | ||||
|       {hiddenRecordFields.length > 0 && ( | ||||
|         <ScrollWrapper contextProviderName="dropdownMenuItemsContainer"> | ||||
|           <ViewFieldsVisibilityDropdownSection | ||||
|             title="Hidden" | ||||
|             fields={hiddenRecordFields} | ||||
|             isDraggable={false} | ||||
|             onVisibilityChange={handleChangeFieldVisibility} | ||||
|             showSubheader={false} | ||||
|             showDragGrip={false} | ||||
|           /> | ||||
|         </ScrollWrapper> | ||||
|       )} | ||||
|       <DropdownMenuSeparator /> | ||||
|  | ||||
|       <UndecoratedLink | ||||
|         to={settingsUrl} | ||||
|         onClick={() => { | ||||
|           setNavigationMemorizedUrl(location.pathname + location.search); | ||||
|           closeDropdown(); | ||||
|         }} | ||||
|       > | ||||
|         <DropdownMenuItemsContainer> | ||||
|           <MenuItem LeftIcon={IconSettings} text="Edit Fields" /> | ||||
|         </DropdownMenuItemsContainer> | ||||
|       </UndecoratedLink> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,103 @@ | ||||
| import { useEffect } from 'react'; | ||||
| import { | ||||
|   IconChevronLeft, | ||||
|   IconSettings, | ||||
|   MenuItem, | ||||
|   UndecoratedLink, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; | ||||
|  | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; | ||||
| import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | ||||
| import { SettingsPath } from '@/types/SettingsPath'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { useLocation } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
|  | ||||
| export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { | ||||
|   const { | ||||
|     currentContentId, | ||||
|     viewType, | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     closeDropdown, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { objectNamePlural } = useObjectNamePluralFromSingular({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { hiddenRecordGroups, viewGroupFieldMetadataItem } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { handleVisibilityChange: handleRecordGroupVisibilityChange } = | ||||
|     useRecordGroupVisibility({ | ||||
|       viewBarId: recordIndexId, | ||||
|       viewType, | ||||
|     }); | ||||
|  | ||||
|   const viewGroupSettingsUrl = getSettingsPagePath( | ||||
|     SettingsPath.ObjectFieldEdit, | ||||
|     { | ||||
|       objectSlug: objectNamePlural, | ||||
|       fieldSlug: viewGroupFieldMetadataItem?.name ?? '', | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   const location = useLocation(); | ||||
|   const setNavigationMemorizedUrl = useSetRecoilState( | ||||
|     navigationMemorizedUrlState, | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       currentContentId === 'hiddenRecordGroups' && | ||||
|       hiddenRecordGroups.length === 0 | ||||
|     ) { | ||||
|       onContentChange('recordGroups'); | ||||
|     } | ||||
|   }, [hiddenRecordGroups, currentContentId, onContentChange]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <DropdownMenuHeader | ||||
|           StartIcon={IconChevronLeft} | ||||
|           onClick={() => onContentChange('recordGroups')} | ||||
|         > | ||||
|           Hidden {viewGroupFieldMetadataItem?.label} | ||||
|         </DropdownMenuHeader> | ||||
|       </DropdownMenuItemsContainer> | ||||
|  | ||||
|       <RecordGroupsVisibilityDropdownSection | ||||
|         title={`Hidden ${viewGroupFieldMetadataItem?.label}`} | ||||
|         recordGroups={hiddenRecordGroups} | ||||
|         onVisibilityChange={handleRecordGroupVisibilityChange} | ||||
|         isDraggable={false} | ||||
|         showSubheader={false} | ||||
|         showDragGrip={false} | ||||
|       /> | ||||
|       <DropdownMenuSeparator /> | ||||
|       <UndecoratedLink | ||||
|         to={viewGroupSettingsUrl} | ||||
|         onClick={() => { | ||||
|           setNavigationMemorizedUrl(location.pathname + location.search); | ||||
|           closeDropdown(); | ||||
|         }} | ||||
|       > | ||||
|         <DropdownMenuItemsContainer> | ||||
|           <MenuItem LeftIcon={IconSettings} text="Edit field values" /> | ||||
|         </DropdownMenuItemsContainer> | ||||
|       </UndecoratedLink> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,145 @@ | ||||
| import { Key } from 'ts-key-enum'; | ||||
| import { | ||||
|   IconFileExport, | ||||
|   IconFileImport, | ||||
|   IconLayout, | ||||
|   IconLayoutList, | ||||
|   IconList, | ||||
|   IconRotate2, | ||||
|   IconTag, | ||||
|   MenuItem, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; | ||||
| import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; | ||||
|  | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { | ||||
|   displayedExportProgress, | ||||
|   useExportRecords, | ||||
| } from '@/object-record/record-index/export/hooks/useExportRecords'; | ||||
| import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; | ||||
| import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; | ||||
|  | ||||
| export const ObjectOptionsDropdownMenuContent = () => { | ||||
|   const { | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     viewType, | ||||
|     onContentChange, | ||||
|     closeDropdown, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED'); | ||||
|  | ||||
|   const { objectNamePlural } = useObjectNamePluralFromSingular({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   useScopedHotkeys( | ||||
|     [Key.Escape], | ||||
|     () => { | ||||
|       closeDropdown(); | ||||
|     }, | ||||
|     TableOptionsHotkeyScope.Dropdown, | ||||
|   ); | ||||
|  | ||||
|   const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } = | ||||
|     useHandleToggleTrashColumnFilter({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   const { visibleBoardFields } = useObjectOptionsForBoard({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|     recordBoardId: recordIndexId, | ||||
|     viewBarId: recordIndexId, | ||||
|   }); | ||||
|  | ||||
|   const { viewGroupFieldMetadataItem } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { openObjectRecordsSpreasheetImportDialog } = | ||||
|     useOpenObjectRecordsSpreadsheetImportDialog( | ||||
|       objectMetadataItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|   const { progress, download } = useExportRecords({ | ||||
|     delayMs: 100, | ||||
|     filename: `${objectMetadataItem.nameSingular}.csv`, | ||||
|     objectMetadataItem, | ||||
|     recordIndexId, | ||||
|     viewType, | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader StartIcon={IconList}> | ||||
|         {objectMetadataItem.labelPlural} | ||||
|       </DropdownMenuHeader> | ||||
|       {/** TODO: Should be removed when view settings contains more options */} | ||||
|       {viewType === ViewType.Kanban && ( | ||||
|         <> | ||||
|           <DropdownMenuItemsContainer> | ||||
|             <MenuItem | ||||
|               onClick={() => onContentChange('viewSettings')} | ||||
|               LeftIcon={IconLayout} | ||||
|               text="View settings" | ||||
|               hasSubMenu | ||||
|             /> | ||||
|           </DropdownMenuItemsContainer> | ||||
|           <DropdownMenuSeparator /> | ||||
|         </> | ||||
|       )} | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <MenuItem | ||||
|           onClick={() => onContentChange('fields')} | ||||
|           LeftIcon={IconTag} | ||||
|           text="Fields" | ||||
|           contextualText={`${visibleBoardFields.length} shown`} | ||||
|           hasSubMenu | ||||
|         /> | ||||
|         {(viewType === ViewType.Kanban || isViewGroupEnabled) && ( | ||||
|           <MenuItem | ||||
|             onClick={() => onContentChange('recordGroups')} | ||||
|             LeftIcon={IconLayoutList} | ||||
|             text="Group by" | ||||
|             contextualText={viewGroupFieldMetadataItem?.label} | ||||
|             hasSubMenu | ||||
|           /> | ||||
|         )} | ||||
|       </DropdownMenuItemsContainer> | ||||
|       <DropdownMenuSeparator /> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <MenuItem | ||||
|           onClick={download} | ||||
|           LeftIcon={IconFileExport} | ||||
|           text={displayedExportProgress(progress)} | ||||
|         /> | ||||
|         <MenuItem | ||||
|           onClick={() => openObjectRecordsSpreasheetImportDialog()} | ||||
|           LeftIcon={IconFileImport} | ||||
|           text="Import" | ||||
|         /> | ||||
|         <MenuItem | ||||
|           onClick={() => { | ||||
|             handleToggleTrashColumnFilter(); | ||||
|             toggleSoftDeleteFilterState(true); | ||||
|             closeDropdown(); | ||||
|           }} | ||||
|           LeftIcon={IconRotate2} | ||||
|           text={`Deleted ${objectNamePlural}`} | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,118 @@ | ||||
| import { useEffect } from 'react'; | ||||
| import { | ||||
|   IconChevronLeft, | ||||
|   IconSettings, | ||||
|   MenuItem, | ||||
|   UndecoratedLink, | ||||
|   useIcons, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; | ||||
|  | ||||
| import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField'; | ||||
| import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | ||||
| import { SettingsPath } from '@/types/SettingsPath'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { useLocation } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
|  | ||||
| export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { | ||||
|   const { getIcon } = useIcons(); | ||||
|  | ||||
|   const { | ||||
|     currentContentId, | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     closeDropdown, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { objectNamePlural } = useObjectNamePluralFromSingular({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { hiddenRecordGroups } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const { | ||||
|     recordGroupFieldSearchInput, | ||||
|     setRecordGroupFieldSearchInput, | ||||
|     filteredRecordGroupFieldMetadataItems, | ||||
|   } = useSearchRecordGroupField(); | ||||
|  | ||||
|   const { handleRecordGroupFieldChange, resetRecordGroupField } = | ||||
|     useHandleRecordGroupField({ | ||||
|       viewBarComponentId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   const newFieldSettingsUrl = getSettingsPagePath( | ||||
|     SettingsPath.ObjectNewFieldSelect, | ||||
|     { | ||||
|       objectSlug: objectNamePlural, | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   const location = useLocation(); | ||||
|   const setNavigationMemorizedUrl = useSetRecoilState( | ||||
|     navigationMemorizedUrlState, | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       currentContentId === 'hiddenRecordGroups' && | ||||
|       hiddenRecordGroups.length === 0 | ||||
|     ) { | ||||
|       onContentChange('recordGroups'); | ||||
|     } | ||||
|   }, [hiddenRecordGroups, currentContentId, onContentChange]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader | ||||
|         StartIcon={IconChevronLeft} | ||||
|         onClick={() => onContentChange('recordGroups')} | ||||
|       > | ||||
|         Group by | ||||
|       </DropdownMenuHeader> | ||||
|       <StyledInput | ||||
|         autoFocus | ||||
|         value={recordGroupFieldSearchInput} | ||||
|         placeholder="Search fields" | ||||
|         onChange={(event) => setRecordGroupFieldSearchInput(event.target.value)} | ||||
|       /> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <MenuItem text="None" onClick={resetRecordGroupField} /> | ||||
|         {filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => ( | ||||
|           <MenuItem | ||||
|             key={fieldMetadataItem.id} | ||||
|             onClick={() => { | ||||
|               handleRecordGroupFieldChange(fieldMetadataItem); | ||||
|             }} | ||||
|             LeftIcon={getIcon(fieldMetadataItem.icon)} | ||||
|             text={fieldMetadataItem.label} | ||||
|           /> | ||||
|         ))} | ||||
|       </DropdownMenuItemsContainer> | ||||
|       <DropdownMenuSeparator /> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <UndecoratedLink | ||||
|           to={newFieldSettingsUrl} | ||||
|           onClick={() => { | ||||
|             setNavigationMemorizedUrl(location.pathname + location.search); | ||||
|             closeDropdown(); | ||||
|           }} | ||||
|         > | ||||
|           <MenuItem LeftIcon={IconSettings} text="Create select field" /> | ||||
|         </UndecoratedLink> | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,79 @@ | ||||
| import { useEffect } from 'react'; | ||||
| import { | ||||
|   IconChevronLeft, | ||||
|   IconHandMove, | ||||
|   IconSortAZ, | ||||
|   IconSortZA, | ||||
|   MenuItem, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; | ||||
| import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; | ||||
|  | ||||
| export const ObjectOptionsDropdownRecordGroupSortContent = () => { | ||||
|   const { | ||||
|     currentContentId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     closeDropdown, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { hiddenRecordGroups } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const setRecordGroupSort = useSetRecoilComponentStateV2( | ||||
|     recordIndexRecordGroupSortComponentState, | ||||
|   ); | ||||
|  | ||||
|   const handleRecordGroupSortChange = (sort: RecordGroupSort) => { | ||||
|     setRecordGroupSort(sort); | ||||
|     closeDropdown(); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       currentContentId === 'hiddenRecordGroups' && | ||||
|       hiddenRecordGroups.length === 0 | ||||
|     ) { | ||||
|       onContentChange('recordGroups'); | ||||
|     } | ||||
|   }, [hiddenRecordGroups, currentContentId, onContentChange]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader | ||||
|         StartIcon={IconChevronLeft} | ||||
|         onClick={() => onContentChange('recordGroups')} | ||||
|       > | ||||
|         Sort | ||||
|       </DropdownMenuHeader> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         <MenuItem | ||||
|           onClick={() => handleRecordGroupSortChange(RecordGroupSort.Manual)} | ||||
|           LeftIcon={IconHandMove} | ||||
|           text={RecordGroupSort.Manual} | ||||
|         /> | ||||
|         <MenuItem | ||||
|           onClick={() => | ||||
|             handleRecordGroupSortChange(RecordGroupSort.Alphabetical) | ||||
|           } | ||||
|           LeftIcon={IconSortAZ} | ||||
|           text={RecordGroupSort.Alphabetical} | ||||
|         /> | ||||
|         <MenuItem | ||||
|           onClick={() => | ||||
|             handleRecordGroupSortChange(RecordGroupSort.ReverseAlphabetical) | ||||
|           } | ||||
|           LeftIcon={IconSortZA} | ||||
|           text={RecordGroupSort.ReverseAlphabetical} | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,138 @@ | ||||
| import { useEffect } from 'react'; | ||||
| import { | ||||
|   IconChevronLeft, | ||||
|   IconCircleOff, | ||||
|   IconEyeOff, | ||||
|   IconLayoutList, | ||||
|   IconSortDescending, | ||||
|   MenuItem, | ||||
|   MenuItemNavigate, | ||||
|   MenuItemToggle, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; | ||||
| import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; | ||||
| import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; | ||||
| import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; | ||||
|  | ||||
| export const ObjectOptionsDropdownRecordGroupsContent = () => { | ||||
|   const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED'); | ||||
|  | ||||
|   const { | ||||
|     currentContentId, | ||||
|     viewType, | ||||
|     recordIndexId, | ||||
|     objectMetadataItem, | ||||
|     onContentChange, | ||||
|     resetContent, | ||||
|   } = useOptionsDropdown(); | ||||
|  | ||||
|   const { | ||||
|     hiddenRecordGroups, | ||||
|     visibleRecordGroups, | ||||
|     viewGroupFieldMetadataItem, | ||||
|   } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const isDragableSortRecordGroup = useRecoilComponentValueV2( | ||||
|     recordIndexRecordGroupIsDraggableSortComponentSelector, | ||||
|   ); | ||||
|  | ||||
|   const hideEmptyRecordGroup = useRecoilComponentValueV2( | ||||
|     recordIndexRecordGroupHideComponentState, | ||||
|   ); | ||||
|  | ||||
|   const { | ||||
|     handleVisibilityChange: handleRecordGroupVisibilityChange, | ||||
|     handleHideEmptyRecordGroupChange, | ||||
|   } = useRecordGroupVisibility({ | ||||
|     viewBarId: recordIndexId, | ||||
|     viewType, | ||||
|   }); | ||||
|  | ||||
|   const { handleOrderChange: handleRecordGroupOrderChange } = | ||||
|     useRecordGroupReorder({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       currentContentId === 'hiddenRecordGroups' && | ||||
|       hiddenRecordGroups.length === 0 | ||||
|     ) { | ||||
|       onContentChange('recordGroups'); | ||||
|     } | ||||
|   }, [hiddenRecordGroups, currentContentId, onContentChange]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}> | ||||
|         Group by | ||||
|       </DropdownMenuHeader> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         {isViewGroupEnabled && ( | ||||
|           <> | ||||
|             <MenuItem | ||||
|               onClick={() => onContentChange('recordGroupFields')} | ||||
|               LeftIcon={IconLayoutList} | ||||
|               text={ | ||||
|                 !viewGroupFieldMetadataItem | ||||
|                   ? 'Group by' | ||||
|                   : `Group by "${viewGroupFieldMetadataItem.label}"` | ||||
|               } | ||||
|               hasSubMenu | ||||
|             /> | ||||
|             <MenuItem | ||||
|               onClick={() => onContentChange('recordGroupSort')} | ||||
|               LeftIcon={IconSortDescending} | ||||
|               text="Sort" | ||||
|               hasSubMenu | ||||
|             /> | ||||
|           </> | ||||
|         )} | ||||
|         <MenuItemToggle | ||||
|           LeftIcon={IconCircleOff} | ||||
|           onToggleChange={handleHideEmptyRecordGroupChange} | ||||
|           toggled={hideEmptyRecordGroup} | ||||
|           text="Hide empty groups" | ||||
|           toggleSize="small" | ||||
|         /> | ||||
|       </DropdownMenuItemsContainer> | ||||
|       {visibleRecordGroups.length > 0 && ( | ||||
|         <> | ||||
|           <DropdownMenuSeparator /> | ||||
|           <RecordGroupsVisibilityDropdownSection | ||||
|             title="Visible groups" | ||||
|             recordGroups={visibleRecordGroups} | ||||
|             onDragEnd={handleRecordGroupOrderChange} | ||||
|             onVisibilityChange={handleRecordGroupVisibilityChange} | ||||
|             isDraggable={isDragableSortRecordGroup} | ||||
|             showDragGrip={true} | ||||
|           /> | ||||
|         </> | ||||
|       )} | ||||
|       {hiddenRecordGroups.length > 0 && ( | ||||
|         <> | ||||
|           <DropdownMenuSeparator /> | ||||
|           <DropdownMenuItemsContainer> | ||||
|             <MenuItemNavigate | ||||
|               onClick={() => onContentChange('hiddenRecordGroups')} | ||||
|               LeftIcon={IconEyeOff} | ||||
|               text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`} | ||||
|             /> | ||||
|           </DropdownMenuItemsContainer> | ||||
|         </> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,50 @@ | ||||
| import { | ||||
|   IconBaselineDensitySmall, | ||||
|   IconChevronLeft, | ||||
|   MenuItemToggle, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
|  | ||||
| export const ObjectOptionsDropdownViewSettingsContent = () => { | ||||
|   const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); | ||||
|  | ||||
|   const { recordIndexId, objectMetadataItem, viewType, resetContent } = | ||||
|     useOptionsDropdown(); | ||||
|  | ||||
|   const { isCompactModeActive, setAndPersistIsCompactModeActive } = | ||||
|     useObjectOptionsForBoard({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       recordBoardId: recordIndexId, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}> | ||||
|         View settings | ||||
|       </DropdownMenuHeader> | ||||
|       <DropdownMenuItemsContainer> | ||||
|         {viewType === ViewType.Kanban && ( | ||||
|           <MenuItemToggle | ||||
|             LeftIcon={IconBaselineDensitySmall} | ||||
|             onToggleChange={() => | ||||
|               setAndPersistIsCompactModeActive( | ||||
|                 !isCompactModeActive, | ||||
|                 currentViewWithCombinedFiltersAndSorts, | ||||
|               ) | ||||
|             } | ||||
|             toggled={isCompactModeActive} | ||||
|             text="Compact view" | ||||
|             toggleSize="small" | ||||
|           /> | ||||
|         )} | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,123 @@ | ||||
| import { Meta, StoryObj } from '@storybook/react'; | ||||
| import { ComponentDecorator } from 'twenty-ui'; | ||||
|  | ||||
| import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; | ||||
| import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; | ||||
| import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownContent'; | ||||
| import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; | ||||
| import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; | ||||
| import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; | ||||
| import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; | ||||
| import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; | ||||
| import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { useEffect } from 'react'; | ||||
| import { MemoryRouter } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
| import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; | ||||
| import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; | ||||
| import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; | ||||
| import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; | ||||
|  | ||||
| const instanceId = 'entity-options-scope'; | ||||
|  | ||||
| const meta: Meta<typeof ObjectOptionsDropdownContent> = { | ||||
|   title: | ||||
|     'Modules/ObjectRecord/ObjectOptionsDropdown/ObjectOptionsDropdownContent', | ||||
|   component: ObjectOptionsDropdownContent, | ||||
|   decorators: [ | ||||
|     (Story) => { | ||||
|       const setObjectMetadataItems = useSetRecoilState( | ||||
|         objectMetadataItemsState, | ||||
|       ); | ||||
|  | ||||
|       useEffect(() => { | ||||
|         setObjectMetadataItems(generatedMockObjectMetadataItems); | ||||
|       }, [setObjectMetadataItems]); | ||||
|  | ||||
|       return ( | ||||
|         <RecordTableComponentInstanceContext.Provider | ||||
|           value={{ instanceId, onColumnsChange: () => {} }} | ||||
|         > | ||||
|           <ViewComponentInstanceContext.Provider value={{ instanceId }}> | ||||
|             <ContextStoreComponentInstanceContext.Provider | ||||
|               value={{ instanceId }} | ||||
|             > | ||||
|               <MemoryRouter | ||||
|                 initialEntries={['/one', '/two', { pathname: '/three' }]} | ||||
|                 initialIndex={1} | ||||
|               > | ||||
|                 <Story /> | ||||
|               </MemoryRouter> | ||||
|             </ContextStoreComponentInstanceContext.Provider> | ||||
|           </ViewComponentInstanceContext.Provider> | ||||
|         </RecordTableComponentInstanceContext.Provider> | ||||
|       ); | ||||
|     }, | ||||
|     ObjectMetadataItemsDecorator, | ||||
|     SnackBarDecorator, | ||||
|     ComponentDecorator, | ||||
|     IconsProviderDecorator, | ||||
|   ], | ||||
|   parameters: { | ||||
|     layout: 'centered', | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default meta; | ||||
| type Story = StoryObj<typeof ObjectOptionsDropdownContent>; | ||||
|  | ||||
| const createStory = (contentId: ObjectOptionsContentId | null): Story => ({ | ||||
|   decorators: [ | ||||
|     (Story) => { | ||||
|       const companyObjectMetadataItem = generatedMockObjectMetadataItems.find( | ||||
|         (item) => item.nameSingular === 'company', | ||||
|       )!; | ||||
|  | ||||
|       return ( | ||||
|         <RecordIndexRootPropsContext.Provider | ||||
|           value={{ | ||||
|             indexIdentifierUrl: () => '', | ||||
|             onIndexRecordsLoaded: () => {}, | ||||
|             onCreateRecord: () => {}, | ||||
|             objectNamePlural: 'companies', | ||||
|             objectNameSingular: 'company', | ||||
|             objectMetadataItem: companyObjectMetadataItem, | ||||
|             recordIndexId: instanceId, | ||||
|           }} | ||||
|         > | ||||
|           <ObjectOptionsDropdownContext.Provider | ||||
|             value={{ | ||||
|               viewType: ViewType.Table, | ||||
|               objectMetadataItem: companyObjectMetadataItem, | ||||
|               recordIndexId: instanceId, | ||||
|               currentContentId: contentId, | ||||
|               onContentChange: () => {}, | ||||
|               resetContent: () => {}, | ||||
|             }} | ||||
|           > | ||||
|             <DropdownMenu> | ||||
|               <Story /> | ||||
|             </DropdownMenu> | ||||
|           </ObjectOptionsDropdownContext.Provider> | ||||
|         </RecordIndexRootPropsContext.Provider> | ||||
|       ); | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
|  | ||||
| export const Default = createStory(null); | ||||
|  | ||||
| export const ViewSettings = createStory('viewSettings'); | ||||
|  | ||||
| export const Fields = createStory('fields'); | ||||
|  | ||||
| export const HiddenFields = createStory('hiddenFields'); | ||||
|  | ||||
| export const RecordGroups = createStory('recordGroups'); | ||||
|  | ||||
| export const RecordGroupFields = createStory('recordGroupFields'); | ||||
|  | ||||
| export const RecordGroupSort = createStory('recordGroupSort'); | ||||
|  | ||||
| export const HiddenRecordGroups = createStory('hiddenRecordGroups'); | ||||
| @@ -0,0 +1 @@ | ||||
| export const OBJECT_OPTIONS_DROPDOWN_ID = 'object-options-dropdown-id'; | ||||
| @@ -0,0 +1,59 @@ | ||||
| import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV'; | ||||
| import { renderHook } from '@testing-library/react'; | ||||
| import { act } from 'react'; | ||||
| import { FieldMetadataType } from '~/generated/graphql'; | ||||
|  | ||||
| jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({ | ||||
|   useObjectMetadataItem: jest.fn(() => ({ | ||||
|     objectMetadataItem: { | ||||
|       fields: [ | ||||
|         { type: FieldMetadataType.Currency, name: 'price' }, | ||||
|         { type: FieldMetadataType.Text, name: 'name' }, | ||||
|       ], | ||||
|     }, | ||||
|   })), | ||||
| })); | ||||
|  | ||||
| describe('useExportProcessRecordsForCSV', () => { | ||||
|   it('processes records with currency fields correctly', () => { | ||||
|     const { result } = renderHook(() => | ||||
|       useExportProcessRecordsForCSV('someObject'), | ||||
|     ); | ||||
|  | ||||
|     const records = [ | ||||
|       { | ||||
|         __typename: 'ObjectRecord', | ||||
|         id: '1', | ||||
|         price: { amountMicros: 123456, currencyCode: 'USD' }, | ||||
|         name: 'Item 1', | ||||
|       }, | ||||
|       { | ||||
|         __typename: 'ObjectRecord', | ||||
|         id: '2', | ||||
|         price: { amountMicros: 789012, currencyCode: 'EUR' }, | ||||
|         name: 'Item 2', | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     let processedRecords; | ||||
|  | ||||
|     act(() => { | ||||
|       processedRecords = result.current.processRecordsForCSVExport(records); | ||||
|     }); | ||||
|  | ||||
|     expect(processedRecords).toEqual([ | ||||
|       { | ||||
|         __typename: 'ObjectRecord', | ||||
|         id: '1', | ||||
|         price: { amountMicros: 0.123456, currencyCode: 'USD' }, | ||||
|         name: 'Item 1', | ||||
|       }, | ||||
|       { | ||||
|         __typename: 'ObjectRecord', | ||||
|         id: '2', | ||||
|         price: { amountMicros: 0.789012, currencyCode: 'EUR' }, | ||||
|         name: 'Item 2', | ||||
|       }, | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,104 @@ | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; | ||||
| import { DropResult, ResponderProvided } from '@hello-pangea/dnd'; | ||||
| import { renderHook } from '@testing-library/react'; | ||||
| import { act } from 'react'; | ||||
| import { RecoilRoot } from 'recoil'; | ||||
|  | ||||
| jest.mock('@/views/hooks/useSaveCurrentViewFields', () => ({ | ||||
|   useSaveCurrentViewFields: jest.fn(() => ({ | ||||
|     saveViewFields: jest.fn(), | ||||
|   })), | ||||
| })); | ||||
|  | ||||
| jest.mock('@/views/hooks/useUpdateCurrentView', () => ({ | ||||
|   useUpdateCurrentView: jest.fn(() => ({ | ||||
|     updateCurrentView: jest.fn(), | ||||
|   })), | ||||
| })); | ||||
|  | ||||
| jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({ | ||||
|   useObjectMetadataItem: jest.fn(() => ({ | ||||
|     objectMetadataItem: { | ||||
|       fields: [ | ||||
|         { | ||||
|           id: 'field1', | ||||
|           name: 'field1', | ||||
|           label: 'Field 1', | ||||
|           isVisible: true, | ||||
|           position: 0, | ||||
|         }, | ||||
|         { | ||||
|           id: 'field2', | ||||
|           name: 'field2', | ||||
|           label: 'Field 2', | ||||
|           isVisible: true, | ||||
|           position: 1, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   })), | ||||
| })); | ||||
|  | ||||
| describe('useObjectOptionsForBoard', () => { | ||||
|   const initialRecoilState = [ | ||||
|     { fieldMetadataId: 'field1', isVisible: true, position: 0 }, | ||||
|     { fieldMetadataId: 'field2', isVisible: true, position: 1 }, | ||||
|   ]; | ||||
|  | ||||
|   const renderWithRecoil = () => | ||||
|     renderHook( | ||||
|       () => | ||||
|         useObjectOptionsForBoard({ | ||||
|           objectNameSingular: 'object', | ||||
|           recordBoardId: 'boardId', | ||||
|           viewBarId: 'viewBarId', | ||||
|         }), | ||||
|       { | ||||
|         wrapper: ({ children }) => ( | ||||
|           <RecoilRoot | ||||
|             initializeState={({ set }) => { | ||||
|               set(recordIndexFieldDefinitionsState, initialRecoilState as any); | ||||
|             }} | ||||
|           > | ||||
|             {children} | ||||
|           </RecoilRoot> | ||||
|         ), | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|   it('reorders fields correctly', () => { | ||||
|     const { result } = renderWithRecoil(); | ||||
|  | ||||
|     const dropResult: DropResult = { | ||||
|       source: { droppableId: 'droppable', index: 1 }, | ||||
|       destination: { droppableId: 'droppable', index: 2 }, | ||||
|       draggableId: 'field1', | ||||
|       type: 'TYPE', | ||||
|       mode: 'FLUID', | ||||
|       reason: 'DROP', | ||||
|       combine: null, | ||||
|     }; | ||||
|  | ||||
|     const responderProvided: ResponderProvided = { | ||||
|       announce: jest.fn(), | ||||
|     }; | ||||
|  | ||||
|     act(() => { | ||||
|       result.current.handleReorderBoardFields(dropResult, responderProvided); | ||||
|     }); | ||||
|  | ||||
|     expect(result.current.visibleBoardFields).toEqual([ | ||||
|       { | ||||
|         fieldMetadataId: 'field2', | ||||
|         isVisible: true, | ||||
|         position: 0, | ||||
|       }, | ||||
|       { | ||||
|         fieldMetadataId: 'field1', | ||||
|         isVisible: true, | ||||
|         position: 1, | ||||
|       }, | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,89 @@ | ||||
| import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; | ||||
| import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; | ||||
| import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; | ||||
| import { DropResult, ResponderProvided } from '@hello-pangea/dnd'; | ||||
| import { renderHook } from '@testing-library/react'; | ||||
| import { act } from 'react'; | ||||
| import { RecoilRoot } from 'recoil'; | ||||
|  | ||||
| describe('useObjectOptionsForTable', () => { | ||||
|   const initialRecoilState = [ | ||||
|     { fieldMetadataId: 'field1', isVisible: true, position: 0 }, | ||||
|     { fieldMetadataId: 'field2', isVisible: true, position: 1 }, | ||||
|     { fieldMetadataId: 'field3', isVisible: true, position: 2 }, | ||||
|     { fieldMetadataId: 'field4', isVisible: true, position: 3 }, | ||||
|     { fieldMetadataId: 'field5', isVisible: true, position: 4 }, | ||||
|   ]; | ||||
|  | ||||
|   const renderWithRecoil = () => | ||||
|     renderHook(() => useObjectOptionsForTable('instance-id'), { | ||||
|       wrapper: ({ children }) => ( | ||||
|         <RecordTableComponentInstanceContext.Provider | ||||
|           value={{ instanceId: 'instance-id', onColumnsChange: jest.fn() }} | ||||
|         > | ||||
|           <RecoilRoot | ||||
|             initializeState={({ set }) => { | ||||
|               set( | ||||
|                 tableColumnsComponentState.atomFamily({ | ||||
|                   instanceId: 'instance-id', | ||||
|                 }), | ||||
|                 initialRecoilState as any, | ||||
|               ); | ||||
|             }} | ||||
|           > | ||||
|             {children} | ||||
|           </RecoilRoot> | ||||
|         </RecordTableComponentInstanceContext.Provider> | ||||
|       ), | ||||
|     }); | ||||
|  | ||||
|   it('reorders table columns correctly', () => { | ||||
|     const { result } = renderWithRecoil(); | ||||
|  | ||||
|     const dropResult = { | ||||
|       source: { droppableId: 'droppable', index: 2 }, | ||||
|       destination: { droppableId: 'droppable', index: 3 }, | ||||
|       draggableId: 'field3', | ||||
|       type: 'TYPE', | ||||
|       mode: 'FLUID', | ||||
|       reason: 'DROP', | ||||
|       combine: null, | ||||
|     } as DropResult; | ||||
|  | ||||
|     const responderProvided = { | ||||
|       announce: jest.fn(), | ||||
|     } as ResponderProvided; | ||||
|  | ||||
|     act(() => { | ||||
|       result.current.handleReorderColumns(dropResult, responderProvided); | ||||
|     }); | ||||
|  | ||||
|     expect(result.current.visibleTableColumns).toEqual([ | ||||
|       { | ||||
|         fieldMetadataId: 'field1', | ||||
|         isVisible: true, | ||||
|         position: 0, | ||||
|       }, | ||||
|       { | ||||
|         fieldMetadataId: 'field3', | ||||
|         isVisible: true, | ||||
|         position: 1, | ||||
|       }, | ||||
|       { | ||||
|         fieldMetadataId: 'field2', | ||||
|         isVisible: true, | ||||
|         position: 2, | ||||
|       }, | ||||
|       { | ||||
|         fieldMetadataId: 'field4', | ||||
|         isVisible: true, | ||||
|         position: 3, | ||||
|       }, | ||||
|       { | ||||
|         fieldMetadataId: 'field5', | ||||
|         isVisible: true, | ||||
|         position: 4, | ||||
|       }, | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,91 @@ | ||||
| import { renderHook } from '@testing-library/react'; | ||||
| import { act } from 'react'; | ||||
|  | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; | ||||
| import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; | ||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
|  | ||||
| jest.mock('@/ui/layout/dropdown/hooks/useDropdown', () => ({ | ||||
|   useDropdown: jest.fn(() => ({ | ||||
|     closeDropdown: jest.fn(), | ||||
|   })), | ||||
| })); | ||||
|  | ||||
| describe('useOptionsDropdown', () => { | ||||
|   const mockOnContentChange = jest.fn(); | ||||
|   const mockCloseDropdown = jest.fn(); | ||||
|   const mockResetContent = jest.fn(); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     jest.mocked(useDropdown).mockReturnValue({ | ||||
|       scopeId: 'mock-scope', | ||||
|       isDropdownOpen: false, | ||||
|       closeDropdown: mockCloseDropdown, | ||||
|       toggleDropdown: jest.fn(), | ||||
|       openDropdown: jest.fn(), | ||||
|       dropdownWidth: undefined, | ||||
|       setDropdownWidth: jest.fn(), | ||||
|       dropdownPlacement: null, | ||||
|       setDropdownPlacement: jest.fn(), | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   afterEach(() => { | ||||
|     jest.clearAllMocks(); | ||||
|   }); | ||||
|  | ||||
|   const renderWithProvider = (contextValue: Partial<any> = {}) => { | ||||
|     const wrapper = ({ children }: any) => ( | ||||
|       <ObjectOptionsDropdownContext.Provider | ||||
|         value={{ | ||||
|           viewType: ViewType.Table, | ||||
|           objectMetadataItem: { | ||||
|             __typename: 'object', | ||||
|             id: '1', | ||||
|             nameSingular: 'company', | ||||
|             namePlural: 'companies', | ||||
|             labelSingular: 'Company', | ||||
|             labelPlural: 'Companies', | ||||
|             icon: 'IconBuildingSkyscraper', | ||||
|             fields: [{}], | ||||
|           } as ObjectMetadataItem, | ||||
|           recordIndexId: 'test-record-index', | ||||
|           currentContentId: 'recordGroups', | ||||
|           onContentChange: mockOnContentChange, | ||||
|           resetContent: mockResetContent, | ||||
|           ...contextValue, | ||||
|         }} | ||||
|       > | ||||
|         {children} | ||||
|       </ObjectOptionsDropdownContext.Provider> | ||||
|     ); | ||||
|     return renderHook(() => useOptionsDropdown(), { wrapper }); | ||||
|   }; | ||||
|  | ||||
|   it('provides closeDropdown functionality from the context', () => { | ||||
|     const { result } = renderWithProvider(); | ||||
|  | ||||
|     act(() => { | ||||
|       result.current.closeDropdown(); | ||||
|     }); | ||||
|  | ||||
|     expect(mockResetContent).toHaveBeenCalled(); | ||||
|     expect(mockCloseDropdown).toHaveBeenCalled(); | ||||
|   }); | ||||
|  | ||||
|   it('returns all context values', () => { | ||||
|     const { result } = renderWithProvider({ | ||||
|       currentContentId: 'fields', | ||||
|     }); | ||||
|  | ||||
|     expect(result.current).toHaveProperty('currentContentId', 'fields'); | ||||
|     expect(result.current).toHaveProperty( | ||||
|       'onContentChange', | ||||
|       mockOnContentChange, | ||||
|     ); | ||||
|     expect(result.current).toHaveProperty('closeDropdown'); | ||||
|     expect(result.current).toHaveProperty('resetContent', mockResetContent); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,65 @@ | ||||
| import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; | ||||
| import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; | ||||
| import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; | ||||
| import { renderHook } from '@testing-library/react'; | ||||
| import { act } from 'react'; | ||||
| import { RecoilRoot } from 'recoil'; | ||||
| import { FieldMetadataType } from '~/generated/graphql'; | ||||
|  | ||||
| describe('useSearchRecordGroupField', () => { | ||||
|   const renderWithContext = (contextValue: any) => | ||||
|     renderHook(() => useSearchRecordGroupField(), { | ||||
|       wrapper: ({ children }) => ( | ||||
|         <RecoilRoot> | ||||
|           <RecordIndexRootPropsContext.Provider value={contextValue}> | ||||
|             <ViewComponentInstanceContext.Provider | ||||
|               value={{ instanceId: 'myViewInstanceId' }} | ||||
|             > | ||||
|               {children} | ||||
|             </ViewComponentInstanceContext.Provider> | ||||
|           </RecordIndexRootPropsContext.Provider> | ||||
|         </RecoilRoot> | ||||
|       ), | ||||
|     }); | ||||
|  | ||||
|   it('filters fields correctly based on input', () => { | ||||
|     const mockContextValue = { | ||||
|       objectMetadataItem: { | ||||
|         fields: [ | ||||
|           { type: FieldMetadataType.Select, label: 'First' }, | ||||
|           { type: FieldMetadataType.Select, label: 'Second' }, | ||||
|           { type: FieldMetadataType.Text, label: 'Third' }, | ||||
|         ], | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     const { result } = renderWithContext(mockContextValue); | ||||
|  | ||||
|     act(() => { | ||||
|       result.current.setRecordGroupFieldSearchInput('First'); | ||||
|     }); | ||||
|  | ||||
|     expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ | ||||
|       { type: FieldMetadataType.Select, label: 'First' }, | ||||
|     ]); | ||||
|   }); | ||||
|  | ||||
|   it('returns all select fields when search input is empty', () => { | ||||
|     const mockContextValue = { | ||||
|       objectMetadataItem: { | ||||
|         fields: [ | ||||
|           { type: FieldMetadataType.Select, label: 'First' }, | ||||
|           { type: FieldMetadataType.Select, label: 'Second' }, | ||||
|           { type: FieldMetadataType.Text, label: 'Third' }, | ||||
|         ], | ||||
|       }, | ||||
|     }; | ||||
|  | ||||
|     const { result } = renderWithContext(mockContextValue); | ||||
|  | ||||
|     expect(result.current.filteredRecordGroupFieldMetadataItems).toEqual([ | ||||
|       { type: FieldMetadataType.Select, label: 'First' }, | ||||
|       { type: FieldMetadataType.Select, label: 'Second' }, | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,39 @@ | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata'; | ||||
| import { ObjectRecord } from '@/object-record/types/ObjectRecord'; | ||||
| import { isDefined } from 'twenty-ui'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { convertCurrencyMicrosToCurrencyAmount } from '~/utils/convertCurrencyToCurrencyMicros'; | ||||
|  | ||||
| export const useExportProcessRecordsForCSV = (objectNameSingular: string) => { | ||||
|   const { objectMetadataItem } = useObjectMetadataItem({ | ||||
|     objectNameSingular, | ||||
|   }); | ||||
|  | ||||
|   const processRecordsForCSVExport = (records: ObjectRecord[]) => { | ||||
|     return records.map((record) => { | ||||
|       const currencyFields = objectMetadataItem.fields.filter( | ||||
|         (field) => field.type === FieldMetadataType.Currency, | ||||
|       ); | ||||
|  | ||||
|       const processedRecord = { | ||||
|         ...record, | ||||
|       }; | ||||
|  | ||||
|       for (const currencyField of currencyFields) { | ||||
|         if (isDefined(record[currencyField.name])) { | ||||
|           processedRecord[currencyField.name] = { | ||||
|             amountMicros: convertCurrencyMicrosToCurrencyAmount( | ||||
|               record[currencyField.name].amountMicros, | ||||
|             ), | ||||
|             currencyCode: record[currencyField.name].currencyCode, | ||||
|           } satisfies FieldCurrencyValue; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return processedRecord; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   return { processRecordsForCSVExport }; | ||||
| }; | ||||
| @@ -16,17 +16,17 @@ import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; | ||||
| import { moveArrayItem } from '~/utils/array/moveArrayItem'; | ||||
| import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; | ||||
| 
 | ||||
| type useRecordIndexOptionsForBoardParams = { | ||||
| type useObjectOptionsForBoardParams = { | ||||
|   objectNameSingular: string; | ||||
|   recordBoardId: string; | ||||
|   viewBarId: string; | ||||
| }; | ||||
| 
 | ||||
| export const useRecordIndexOptionsForBoard = ({ | ||||
| export const useObjectOptionsForBoard = ({ | ||||
|   objectNameSingular, | ||||
|   recordBoardId, | ||||
|   viewBarId, | ||||
| }: useRecordIndexOptionsForBoardParams) => { | ||||
| }: useObjectOptionsForBoardParams) => { | ||||
|   const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] = | ||||
|     useRecoilState(recordIndexFieldDefinitionsState); | ||||
| 
 | ||||
| @@ -7,7 +7,7 @@ import { visibleTableColumnsComponentSelector } from '@/object-record/record-tab | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { moveArrayItem } from '~/utils/array/moveArrayItem'; | ||||
| 
 | ||||
| export const useRecordIndexOptionsForTable = (recordTableId: string) => { | ||||
| export const useObjectOptionsForTable = (recordTableId: string) => { | ||||
|   const hiddenTableColumns = useRecoilComponentValueV2( | ||||
|     hiddenTableColumnsComponentSelector, | ||||
|     recordTableId, | ||||
| @@ -0,0 +1,26 @@ | ||||
| import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; | ||||
| import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; | ||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||
| import { useCallback, useContext } from 'react'; | ||||
|  | ||||
| export const useOptionsDropdown = () => { | ||||
|   const { closeDropdown } = useDropdown(OBJECT_OPTIONS_DROPDOWN_ID); | ||||
|  | ||||
|   const context = useContext(ObjectOptionsDropdownContext); | ||||
|  | ||||
|   if (!context) { | ||||
|     throw new Error( | ||||
|       'useOptionsDropdown must be used within a ObjectOptionsDropdownContext.Provider', | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const handleCloseDropdown = useCallback(() => { | ||||
|     context.resetContent(); | ||||
|     closeDropdown(); | ||||
|   }, [closeDropdown, context]); | ||||
|  | ||||
|   return { | ||||
|     ...context, | ||||
|     closeDropdown: handleCloseDropdown, | ||||
|   }; | ||||
| }; | ||||
| @@ -0,0 +1,29 @@ | ||||
| import { objectOptionsDropdownSearchInputComponentState } from '@/object-record/object-options-dropdown/states/objectOptionsDropdownSearchInputComponentState'; | ||||
| import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; | ||||
| import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; | ||||
| import { useContext, useMemo } from 'react'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
|  | ||||
| export const useSearchRecordGroupField = () => { | ||||
|   const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); | ||||
|  | ||||
|   const [recordGroupFieldSearchInput, setRecordGroupFieldSearchInput] = | ||||
|     useRecoilComponentStateV2(objectOptionsDropdownSearchInputComponentState); | ||||
|  | ||||
|   const filteredRecordGroupFieldMetadataItems = useMemo(() => { | ||||
|     const searchInputLowerCase = | ||||
|       recordGroupFieldSearchInput.toLocaleLowerCase(); | ||||
|  | ||||
|     return objectMetadataItem.fields.filter( | ||||
|       (field) => | ||||
|         field.type === FieldMetadataType.Select && | ||||
|         field.label.toLocaleLowerCase().includes(searchInputLowerCase), | ||||
|     ); | ||||
|   }, [objectMetadataItem.fields, recordGroupFieldSearchInput]); | ||||
|  | ||||
|   return { | ||||
|     recordGroupFieldSearchInput, | ||||
|     setRecordGroupFieldSearchInput, | ||||
|     filteredRecordGroupFieldMetadataItems, | ||||
|   }; | ||||
| }; | ||||
| @@ -0,0 +1,18 @@ | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { createContext } from 'react'; | ||||
|  | ||||
| export type ObjectOptionsDropdownContextValue = { | ||||
|   recordIndexId: string; | ||||
|   objectMetadataItem: ObjectMetadataItem; | ||||
|   viewType: ViewType; | ||||
|   currentContentId: ObjectOptionsContentId | null; | ||||
|   onContentChange: (key: ObjectOptionsContentId) => void; | ||||
|   resetContent: () => void; | ||||
| }; | ||||
|  | ||||
| export const ObjectOptionsDropdownContext = | ||||
|   createContext<ObjectOptionsDropdownContextValue>( | ||||
|     {} as ObjectOptionsDropdownContextValue, | ||||
|   ); | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; | ||||
| import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; | ||||
|  | ||||
| export const objectOptionsDropdownSearchInputComponentState = | ||||
|   createComponentStateV2<string>({ | ||||
|     key: 'objectOptionsDropdownSearchInputComponentState', | ||||
|     defaultValue: '', | ||||
|     componentInstanceContext: ViewComponentInstanceContext, | ||||
|   }); | ||||
| @@ -0,0 +1,8 @@ | ||||
| export type ObjectOptionsContentId = | ||||
|   | 'viewSettings' | ||||
|   | 'fields' | ||||
|   | 'hiddenFields' | ||||
|   | 'recordGroups' | ||||
|   | 'hiddenRecordGroups' | ||||
|   | 'recordGroupFields' | ||||
|   | 'recordGroupSort'; | ||||
| @@ -2,12 +2,20 @@ import { useRecoilCallback } from 'recoil'; | ||||
|  | ||||
| import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; | ||||
| import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; | ||||
| import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; | ||||
|  | ||||
| export const useSetRecordBoardColumns = (recordBoardId?: string) => { | ||||
|   const { scopeId, columnIdsState, columnsFamilySelector } = | ||||
|     useRecordBoardStates(recordBoardId); | ||||
|  | ||||
|   const recordGroupSort = useRecoilComponentValueV2( | ||||
|     recordIndexRecordGroupSortComponentState, | ||||
|     recordBoardId, | ||||
|   ); | ||||
|  | ||||
|   const setColumns = useRecoilCallback( | ||||
|     ({ set, snapshot }) => | ||||
|       (columns: RecordGroupDefinition[]) => { | ||||
| @@ -15,7 +23,12 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { | ||||
|           .getLoadable(columnIdsState) | ||||
|           .getValue(); | ||||
|  | ||||
|         const columnIds = columns | ||||
|         const sortedColumns = sortRecordGroupDefinitions( | ||||
|           columns, | ||||
|           recordGroupSort, | ||||
|         ); | ||||
|  | ||||
|         const columnIds = sortedColumns | ||||
|           .filter(({ isVisible }) => isVisible) | ||||
|           .map(({ id }) => id); | ||||
|  | ||||
| @@ -35,7 +48,7 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { | ||||
|           set(columnsFamilySelector(column.id), column); | ||||
|         }); | ||||
|       }, | ||||
|     [columnsFamilySelector, columnIdsState], | ||||
|     [columnIdsState, recordGroupSort, columnsFamilySelector], | ||||
|   ); | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRec | ||||
| import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { MenuItem } from 'twenty-ui'; | ||||
|  | ||||
| const StyledMenuContainer = styled.div` | ||||
| @@ -25,7 +26,9 @@ export const RecordBoardColumnDropdownMenu = ({ | ||||
| }: RecordBoardColumnDropdownMenuProps) => { | ||||
|   const boardColumnMenuRef = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   const recordGroupActions = useRecordGroupActions(); | ||||
|   const recordGroupActions = useRecordGroupActions({ | ||||
|     viewType: ViewType.Kanban, | ||||
|   }); | ||||
|  | ||||
|   const closeMenu = useCallback(() => { | ||||
|     onClose(); | ||||
|   | ||||
| @@ -19,9 +19,8 @@ import { toSpliced } from '~/utils/array/toSpliced'; | ||||
| import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; | ||||
|  | ||||
| const StyledDropdownMenu = styled(DropdownMenu)` | ||||
|   margin-left: -1px; | ||||
|   margin: -1px; | ||||
|   position: relative; | ||||
|   margin-top: -1px; | ||||
| `; | ||||
|  | ||||
| type MultiItemFieldInputProps<T> = { | ||||
| @@ -65,8 +64,12 @@ export const MultiItemFieldInput = <T,>({ | ||||
|   }; | ||||
|  | ||||
|   const handleDropdownCloseOutside = (event: MouseEvent | TouchEvent) => { | ||||
|     onCancel?.(); | ||||
|     event.stopImmediatePropagation(); | ||||
|     if (inputValue?.trim().length > 0) { | ||||
|       handleSubmitInput(); | ||||
|     } else { | ||||
|       onCancel?.(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   useListenClickOutside({ | ||||
| @@ -202,10 +205,12 @@ export const MultiItemFieldInput = <T,>({ | ||||
|           } | ||||
|           onEnter={handleSubmitInput} | ||||
|           rightComponent={ | ||||
|             <LightIconButton | ||||
|               Icon={isAddingNewItem ? IconPlus : IconCheck} | ||||
|               onClick={handleSubmitInput} | ||||
|             /> | ||||
|             items.length ? ( | ||||
|               <LightIconButton | ||||
|                 Icon={isAddingNewItem ? IconPlus : IconCheck} | ||||
|                 onClick={handleSubmitInput} | ||||
|               /> | ||||
|             ) : null | ||||
|           } | ||||
|         /> | ||||
|       ) : ( | ||||
|   | ||||
| @@ -18,6 +18,9 @@ export const isMatchingSelectFilter = ({ | ||||
|         return value !== null; | ||||
|       } | ||||
|     } | ||||
|     case selectFilter.eq !== undefined: { | ||||
|       return value === selectFilter.eq; | ||||
|     } | ||||
|     default: { | ||||
|       throw new Error( | ||||
|         `Unexpected value for select filter : ${JSON.stringify(selectFilter)}`, | ||||
|   | ||||
| @@ -0,0 +1,65 @@ | ||||
| import { IconEye, IconEyeOff, MenuItemDraggable, Tag } from 'twenty-ui'; | ||||
|  | ||||
| import { | ||||
|   RecordGroupDefinition, | ||||
|   RecordGroupDefinitionType, | ||||
| } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
| type RecordGroupMenuItemDraggableProps = { | ||||
|   recordGroup: RecordGroupDefinition; | ||||
|   showDragGrip?: boolean; | ||||
|   isDraggable?: boolean; | ||||
|   onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; | ||||
| }; | ||||
|  | ||||
| export const RecordGroupMenuItemDraggable = ({ | ||||
|   recordGroup, | ||||
|   showDragGrip, | ||||
|   isDraggable, | ||||
|   onVisibilityChange, | ||||
| }: RecordGroupMenuItemDraggableProps) => { | ||||
|   const isNoValue = recordGroup.type === RecordGroupDefinitionType.NoValue; | ||||
|  | ||||
|   const getIconButtons = (recordGroup: RecordGroupDefinition) => { | ||||
|     const iconButtons = [ | ||||
|       { | ||||
|         Icon: recordGroup.isVisible ? IconEyeOff : IconEye, | ||||
|         onClick: () => onVisibilityChange(recordGroup), | ||||
|       }, | ||||
|     ].filter(isDefined); | ||||
|  | ||||
|     return iconButtons.length ? iconButtons : undefined; | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <MenuItemDraggable | ||||
|       key={recordGroup.id} | ||||
|       text={ | ||||
|         <Tag | ||||
|           variant={ | ||||
|             recordGroup.type !== RecordGroupDefinitionType.NoValue | ||||
|               ? 'solid' | ||||
|               : 'outline' | ||||
|           } | ||||
|           color={ | ||||
|             recordGroup.type !== RecordGroupDefinitionType.NoValue | ||||
|               ? recordGroup.color | ||||
|               : 'transparent' | ||||
|           } | ||||
|           text={recordGroup.title} | ||||
|           weight={ | ||||
|             recordGroup.type !== RecordGroupDefinitionType.NoValue | ||||
|               ? 'regular' | ||||
|               : 'medium' | ||||
|           } | ||||
|         /> | ||||
|       } | ||||
|       accent={isNoValue || showDragGrip ? 'placeholder' : 'default'} | ||||
|       iconButtons={!isNoValue ? getIconButtons(recordGroup) : undefined} | ||||
|       showGrip={isNoValue ? true : showDragGrip} | ||||
|       isDragDisabled={isNoValue ? true : !isDraggable} | ||||
|       isHoverDisabled={isNoValue} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,106 @@ | ||||
| import { | ||||
|   DropResult, | ||||
|   OnDragEndResponder, | ||||
|   ResponderProvided, | ||||
| } from '@hello-pangea/dnd'; | ||||
| import { useRef } from 'react'; | ||||
|  | ||||
| import { RecordGroupMenuItemDraggable } from '@/object-record/record-group/components/RecordGroupMenuItemDraggable'; | ||||
| import { | ||||
|   RecordGroupDefinition, | ||||
|   RecordGroupDefinitionType, | ||||
| } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; | ||||
| import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; | ||||
|  | ||||
| type RecordGroupsVisibilityDropdownSectionProps = { | ||||
|   recordGroups: RecordGroupDefinition[]; | ||||
|   isDraggable: boolean; | ||||
|   onDragEnd?: OnDragEndResponder; | ||||
|   onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; | ||||
|   title: string; | ||||
|   showSubheader?: boolean; | ||||
|   showDragGrip: boolean; | ||||
| }; | ||||
|  | ||||
| export const RecordGroupsVisibilityDropdownSection = ({ | ||||
|   recordGroups, | ||||
|   isDraggable, | ||||
|   onDragEnd, | ||||
|   onVisibilityChange, | ||||
|   title, | ||||
|   showSubheader = true, | ||||
|   showDragGrip, | ||||
| }: RecordGroupsVisibilityDropdownSectionProps) => { | ||||
|   const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { | ||||
|     onDragEnd?.(result, provided); | ||||
|   }; | ||||
|  | ||||
|   const noValueRecordGroups = | ||||
|     recordGroups.filter( | ||||
|       (recordGroup) => recordGroup.type === RecordGroupDefinitionType.NoValue, | ||||
|     ) ?? []; | ||||
|  | ||||
|   const recordGroupsWithoutNoValueGroups = recordGroups.filter( | ||||
|     (recordGroup) => recordGroup.type !== RecordGroupDefinitionType.NoValue, | ||||
|   ); | ||||
|  | ||||
|   const ref = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   return ( | ||||
|     <div ref={ref}> | ||||
|       {showSubheader && ( | ||||
|         <StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader> | ||||
|       )} | ||||
|       <DropdownMenuItemsContainer> | ||||
|         {!!recordGroups.length && ( | ||||
|           <> | ||||
|             {!isDraggable ? ( | ||||
|               recordGroupsWithoutNoValueGroups.map((recordGroup) => ( | ||||
|                 <RecordGroupMenuItemDraggable | ||||
|                   recordGroup={recordGroup} | ||||
|                   onVisibilityChange={onVisibilityChange} | ||||
|                   showDragGrip={showDragGrip} | ||||
|                   isDraggable={isDraggable} | ||||
|                 /> | ||||
|               )) | ||||
|             ) : ( | ||||
|               <DraggableList | ||||
|                 onDragEnd={handleOnDrag} | ||||
|                 draggableItems={ | ||||
|                   <> | ||||
|                     {recordGroupsWithoutNoValueGroups.map( | ||||
|                       (recordGroup, index) => ( | ||||
|                         <DraggableItem | ||||
|                           key={recordGroup.id} | ||||
|                           draggableId={recordGroup.id} | ||||
|                           index={index + 1} | ||||
|                           itemComponent={ | ||||
|                             <RecordGroupMenuItemDraggable | ||||
|                               recordGroup={recordGroup} | ||||
|                               onVisibilityChange={onVisibilityChange} | ||||
|                               showDragGrip={showDragGrip} | ||||
|                               isDraggable={isDraggable} | ||||
|                             /> | ||||
|                           } | ||||
|                         /> | ||||
|                       ), | ||||
|                     )} | ||||
|                   </> | ||||
|                 } | ||||
|               /> | ||||
|             )} | ||||
|             {noValueRecordGroups.map((recordGroup) => ( | ||||
|               <RecordGroupMenuItemDraggable | ||||
|                 recordGroup={recordGroup} | ||||
|                 onVisibilityChange={onVisibilityChange} | ||||
|               /> | ||||
|             ))} | ||||
|           </> | ||||
|         )} | ||||
|       </DropdownMenuItemsContainer> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -8,12 +8,19 @@ import { RecordGroupAction } from '@/object-record/record-group/types/RecordGrou | ||||
| import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { useCallback, useContext, useMemo } from 'react'; | ||||
| import { useLocation, useNavigate } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
| import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; | ||||
|  | ||||
| export const useRecordGroupActions = () => { | ||||
| type UseRecordGroupActionsParams = { | ||||
|   viewType: ViewType; | ||||
| }; | ||||
|  | ||||
| export const useRecordGroupActions = ({ | ||||
|   viewType, | ||||
| }: UseRecordGroupActionsParams) => { | ||||
|   const navigate = useNavigate(); | ||||
|   const location = useLocation(); | ||||
|  | ||||
| @@ -36,6 +43,7 @@ export const useRecordGroupActions = () => { | ||||
|   const { handleVisibilityChange: handleRecordGroupVisibilityChange } = | ||||
|     useRecordGroupVisibility({ | ||||
|       viewBarId: recordIndexId, | ||||
|       viewType, | ||||
|     }); | ||||
|  | ||||
|   const setNavigationMemorizedUrl = useSetRecoilState( | ||||
|   | ||||
| @@ -1,45 +1,130 @@ | ||||
| import { useCallback } from 'react'; | ||||
|  | ||||
| import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; | ||||
| import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; | ||||
| import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; | ||||
| import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; | ||||
| import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; | ||||
| import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; | ||||
| import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; | ||||
| import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; | ||||
| import { useRecoilCallback } from 'recoil'; | ||||
|  | ||||
| type UseRecordGroupVisibilityParams = { | ||||
|   viewBarId: string; | ||||
|   viewType: ViewType; | ||||
| }; | ||||
|  | ||||
| export const useRecordGroupVisibility = ({ | ||||
|   viewBarId, | ||||
|   viewType, | ||||
| }: UseRecordGroupVisibilityParams) => { | ||||
|   const [recordGroupDefinitions, setRecordGroupDefinitions] = | ||||
|     useRecoilComponentStateV2(recordGroupDefinitionsComponentState); | ||||
|   const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( | ||||
|     recordGroupDefinitionsComponentState, | ||||
|   ); | ||||
|  | ||||
|   const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( | ||||
|     tableRowIdsByGroupComponentFamilyState, | ||||
|     viewBarId, | ||||
|   ); | ||||
|  | ||||
|   const { recordIdsByColumnIdFamilyState } = useRecordBoardStates(viewBarId); | ||||
|  | ||||
|   const objectOptionsDropdownRecordGroupHideState = | ||||
|     useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); | ||||
|  | ||||
|   const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); | ||||
|  | ||||
|   const handleVisibilityChange = useCallback( | ||||
|     async (updatedRecordGroupDefinition: RecordGroupDefinition) => { | ||||
|       const updatedRecordGroupDefinitions = recordGroupDefinitions.map( | ||||
|         (groupDefinition) => | ||||
|           groupDefinition.id === updatedRecordGroupDefinition.id | ||||
|             ? { | ||||
|                 ...groupDefinition, | ||||
|                 isVisible: !groupDefinition.isVisible, | ||||
|               } | ||||
|             : groupDefinition, | ||||
|       ); | ||||
|   const handleVisibilityChange = useRecoilCallback( | ||||
|     ({ snapshot, set }) => | ||||
|       async (updatedRecordGroupDefinition: RecordGroupDefinition) => { | ||||
|         const recordGroupDefinitions = getSnapshotValue( | ||||
|           snapshot, | ||||
|           recordGroupDefinitionsState, | ||||
|         ); | ||||
|  | ||||
|       setRecordGroupDefinitions(updatedRecordGroupDefinitions); | ||||
|         const updatedRecordGroupDefinitions = recordGroupDefinitions.map( | ||||
|           (groupDefinition) => | ||||
|             groupDefinition.id === updatedRecordGroupDefinition.id | ||||
|               ? { | ||||
|                   ...groupDefinition, | ||||
|                   isVisible: !groupDefinition.isVisible, | ||||
|                 } | ||||
|               : groupDefinition, | ||||
|         ); | ||||
|  | ||||
|       saveViewGroups( | ||||
|         mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), | ||||
|       ); | ||||
|     }, | ||||
|     [recordGroupDefinitions, setRecordGroupDefinitions, saveViewGroups], | ||||
|         set(recordGroupDefinitionsState, updatedRecordGroupDefinitions); | ||||
|  | ||||
|         saveViewGroups( | ||||
|           mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), | ||||
|         ); | ||||
|  | ||||
|         // If visibility is manually toggled, we should reset the hideEmptyRecordGroup state | ||||
|         set(objectOptionsDropdownRecordGroupHideState, false); | ||||
|       }, | ||||
|     [ | ||||
|       objectOptionsDropdownRecordGroupHideState, | ||||
|       recordGroupDefinitionsState, | ||||
|       saveViewGroups, | ||||
|     ], | ||||
|   ); | ||||
|  | ||||
|   const handleHideEmptyRecordGroupChange = useRecoilCallback( | ||||
|     ({ snapshot, set }) => | ||||
|       async () => { | ||||
|         const recordGroupDefinitions = getSnapshotValue( | ||||
|           snapshot, | ||||
|           recordGroupDefinitionsState, | ||||
|         ); | ||||
|  | ||||
|         const currentHideState = getSnapshotValue( | ||||
|           snapshot, | ||||
|           objectOptionsDropdownRecordGroupHideState, | ||||
|         ); | ||||
|  | ||||
|         set(objectOptionsDropdownRecordGroupHideState, !currentHideState); | ||||
|  | ||||
|         const updatedRecordGroupDefinitions = recordGroupDefinitions.map( | ||||
|           (recordGroup) => { | ||||
|             // TODO: Maybe we can improve that and only use one state for both table and board | ||||
|             const recordGroupRowIds = | ||||
|               viewType === ViewType.Table | ||||
|                 ? getSnapshotValue( | ||||
|                     snapshot, | ||||
|                     tableRowIdsByGroupFamilyState(recordGroup.id), | ||||
|                   ) | ||||
|                 : getSnapshotValue( | ||||
|                     snapshot, | ||||
|                     recordIdsByColumnIdFamilyState(recordGroup.id), | ||||
|                   ); | ||||
|  | ||||
|             if (recordGroupRowIds.length > 0) { | ||||
|               return recordGroup; | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|               ...recordGroup, | ||||
|               isVisible: currentHideState, | ||||
|             }; | ||||
|           }, | ||||
|         ); | ||||
|  | ||||
|         saveViewGroups( | ||||
|           mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), | ||||
|         ); | ||||
|       }, | ||||
|     [ | ||||
|       recordGroupDefinitionsState, | ||||
|       objectOptionsDropdownRecordGroupHideState, | ||||
|       saveViewGroups, | ||||
|       viewType, | ||||
|       tableRowIdsByGroupFamilyState, | ||||
|       recordIdsByColumnIdFamilyState, | ||||
|     ], | ||||
|   ); | ||||
|  | ||||
|   return { | ||||
|     handleVisibilityChange, | ||||
|     handleHideEmptyRecordGroupChange, | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import { useMemo } from 'react'; | ||||
|  | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; | ||||
| import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; | ||||
| import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
|  | ||||
| type UseRecordGroupsParams = { | ||||
| @@ -15,6 +17,10 @@ export const useRecordGroups = ({ | ||||
|     recordGroupDefinitionsComponentState, | ||||
|   ); | ||||
|  | ||||
|   const recordGroupSort = useRecoilComponentValueV2( | ||||
|     recordIndexRecordGroupSortComponentState, | ||||
|   ); | ||||
|  | ||||
|   const { objectMetadataItem } = useObjectMetadataItem({ | ||||
|     objectNameSingular, | ||||
|   }); | ||||
| @@ -35,14 +41,8 @@ export const useRecordGroups = ({ | ||||
|   }, [objectMetadataItem, recordGroupDefinitions]); | ||||
|  | ||||
|   const visibleRecordGroups = useMemo( | ||||
|     () => | ||||
|       recordGroupDefinitions | ||||
|         .filter((boardGroup) => boardGroup.isVisible) | ||||
|         .sort( | ||||
|           (boardGroupA, boardGroupB) => | ||||
|             boardGroupA.position - boardGroupB.position, | ||||
|         ), | ||||
|     [recordGroupDefinitions], | ||||
|     () => sortRecordGroupDefinitions(recordGroupDefinitions, recordGroupSort), | ||||
|     [recordGroupDefinitions, recordGroupSort], | ||||
|   ); | ||||
|  | ||||
|   const hiddenRecordGroups = useMemo( | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| export enum RecordGroupSort { | ||||
|   Manual = 'Manual', | ||||
|   Alphabetical = 'Alphabetical', | ||||
|   ReverseAlphabetical = 'Reverse Alphabetical', | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; | ||||
| import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; | ||||
|  | ||||
| export const sortRecordGroupDefinitions = ( | ||||
|   recordGroupDefinitions: RecordGroupDefinition[], | ||||
|   recordGroupSort: RecordGroupSort, | ||||
| ) => { | ||||
|   const visibleGroups = recordGroupDefinitions.filter( | ||||
|     (boardGroup) => boardGroup.isVisible, | ||||
|   ); | ||||
|  | ||||
|   const compareAlphabetical = (a: string, b: string, reverse = false) => { | ||||
|     if (a < b) return reverse ? 1 : -1; | ||||
|     if (a > b) return reverse ? -1 : 1; | ||||
|     return 0; | ||||
|   }; | ||||
|  | ||||
|   switch (recordGroupSort) { | ||||
|     case RecordGroupSort.Alphabetical: | ||||
|       return visibleGroups.sort((a, b) => | ||||
|         compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase()), | ||||
|       ); | ||||
|     case RecordGroupSort.ReverseAlphabetical: | ||||
|       return visibleGroups.sort((a, b) => | ||||
|         compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase(), true), | ||||
|       ); | ||||
|     case RecordGroupSort.Manual: | ||||
|     default: | ||||
|       return visibleGroups.sort((a, b) => a.position - b.position); | ||||
|   } | ||||
| }; | ||||
| @@ -30,7 +30,7 @@ export const RecordIndexBoardDataLoaderEffect = ({ | ||||
|     recordIndexFieldDefinitionsState, | ||||
|   ); | ||||
|  | ||||
|   const recordIndexGroupDefinitions = useRecoilComponentValueV2( | ||||
|   const recordGroupDefinitions = useRecoilComponentValueV2( | ||||
|     recordGroupDefinitionsComponentState, | ||||
|   ); | ||||
|  | ||||
| @@ -67,8 +67,8 @@ export const RecordIndexBoardDataLoaderEffect = ({ | ||||
|   }, [objectNameSingular, setObjectSingularName]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setColumns(recordIndexGroupDefinitions); | ||||
|   }, [recordIndexGroupDefinitions, setColumns]); | ||||
|     setColumns(recordGroupDefinitions); | ||||
|   }, [recordGroupDefinitions, setColumns]); | ||||
|  | ||||
|   // TODO: Remove this duplicate useEffect by ensuring it's not here because | ||||
|   // We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern | ||||
|   | ||||
| @@ -2,13 +2,13 @@ import styled from '@emotion/styled'; | ||||
| import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil'; | ||||
|  | ||||
| import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; | ||||
| import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown'; | ||||
| import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer'; | ||||
| import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader'; | ||||
| import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect'; | ||||
| import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer'; | ||||
| import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect'; | ||||
| import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect'; | ||||
| import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; | ||||
| import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; | ||||
| import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; | ||||
| import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; | ||||
| @@ -162,7 +162,7 @@ export const RecordIndexContainer = () => { | ||||
|           <ViewBar | ||||
|             viewBarId={recordIndexId} | ||||
|             optionsDropdownButton={ | ||||
|               <RecordIndexOptionsDropdown | ||||
|               <ObjectOptionsDropdown | ||||
|                 recordIndexId={recordIndexId} | ||||
|                 objectMetadataItem={objectMetadataItem} | ||||
|                 viewType={recordIndexViewType ?? ViewType.Table} | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { PageFavoriteFoldersDropdown } from '@/favorites/components/PageFavoriteFolderDropdown'; | ||||
| import { FAVORITE_FOLDER_PICKER_DROPDOWN_ID } from '@/favorites/favorite-folder-picker/constants/FavoriteFolderPickerDropdownId'; | ||||
| import { useFavorites } from '@/favorites/hooks/useFavorites'; | ||||
| import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; | ||||
| @@ -8,7 +9,6 @@ import { recordIndexViewTypeState } from '@/object-record/record-index/states/re | ||||
| import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; | ||||
| import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; | ||||
| import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; | ||||
| import { PageFavoriteFoldersDropdown } from '@/ui/layout/page/components/PageFavoriteFolderDropdown'; | ||||
| import { PageHeader } from '@/ui/layout/page/components/PageHeader'; | ||||
| import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| @@ -39,7 +39,7 @@ export const RecordIndexPageHeader = () => { | ||||
|  | ||||
|   const view = views.find((view) => view.id === currentViewId); | ||||
|  | ||||
|   const favorites = useFavorites(); | ||||
|   const { sortedFavorites: favorites } = useFavorites(); | ||||
|  | ||||
|   const isFavorite = favorites.some( | ||||
|     (favorite) => | ||||
|   | ||||
| @@ -7,9 +7,9 @@ import { | ||||
| } from '../useExportFetchRecords'; | ||||
|  | ||||
| import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; | ||||
| import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; | ||||
| import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; | ||||
| import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { MockedResponse } from '@apollo/client/testing'; | ||||
| @@ -248,7 +248,7 @@ describe('useRecordData', () => { | ||||
|             }), | ||||
|             useRecordBoardHook: useRecordBoard(recordIndexId), | ||||
|             kanbanFieldName: useRecoilValue(kanbanFieldNameState), | ||||
|             kanbanData: useRecordIndexOptionsForBoard({ | ||||
|             kanbanData: useObjectOptionsForBoard({ | ||||
|               objectNameSingular: objectMetadataItem.nameSingular, | ||||
|               recordBoardId: recordIndexId, | ||||
|               viewBarId: recordIndexId, | ||||
| @@ -338,7 +338,7 @@ describe('useRecordData', () => { | ||||
|             }), | ||||
|             setKanbanFieldName: useRecordBoard(recordIndexId), | ||||
|             kanbanFieldName: useRecoilValue(kanbanFieldNameState), | ||||
|             kanbanData: useRecordIndexOptionsForBoard({ | ||||
|             kanbanData: useObjectOptionsForBoard({ | ||||
|               objectNameSingular: objectMetadataItem.nameSingular, | ||||
|               recordBoardId: recordIndexId, | ||||
|               viewBarId: recordIndexId, | ||||
|   | ||||
| @@ -86,7 +86,7 @@ describe('csvDownloader', () => { | ||||
|  | ||||
| describe('displayedExportProgress', () => { | ||||
|   it.each([ | ||||
|     [undefined, undefined, 'percentage', 'Export View as CSV'], | ||||
|     [undefined, undefined, 'percentage', 'Export'], | ||||
|     [20, 50, 'percentage', 'Export (40%)'], | ||||
|     [0, 100, 'number', 'Export (0)'], | ||||
|     [10, 10, 'percentage', 'Export (100%)'], | ||||
| @@ -96,7 +96,7 @@ describe('displayedExportProgress', () => { | ||||
|     'displays the export progress', | ||||
|     (exportedRecordCount, totalRecordCount, displayType, expected) => { | ||||
|       expect( | ||||
|         displayedExportProgress('all', { | ||||
|         displayedExportProgress({ | ||||
|           exportedRecordCount, | ||||
|           totalRecordCount, | ||||
|           displayType: displayType as 'percentage' | 'number', | ||||
|   | ||||
| @@ -11,10 +11,10 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s | ||||
| import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; | ||||
| import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; | ||||
| import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; | ||||
| import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; | ||||
| import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; | ||||
| import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; | ||||
| import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; | ||||
| import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| @@ -62,7 +62,7 @@ export const useExportFetchRecords = ({ | ||||
|   }); | ||||
|   const [previousRecordCount, setPreviousRecordCount] = useState(0); | ||||
|  | ||||
|   const { hiddenBoardFields } = useRecordIndexOptionsForBoard({ | ||||
|   const { hiddenBoardFields } = useObjectOptionsForBoard({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|     recordBoardId: recordIndexId, | ||||
|     viewBarId: recordIndexId, | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { json2csv } from 'json-2-csv'; | ||||
| import { useMemo } from 'react'; | ||||
|  | ||||
| import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; | ||||
| import { useExportProcessRecordsForCSV } from '@/object-record/object-options-dropdown/hooks/useExportProcessRecordsForCSV'; | ||||
| import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; | ||||
| import { | ||||
|   UseRecordDataOptions, | ||||
|   useExportFetchRecords, | ||||
| } from '@/object-record/record-index/export/hooks/useExportFetchRecords'; | ||||
| import { useExportProcessRecordsForCSV } from '@/object-record/record-index/export/hooks/useExportProcessRecordsForCSV'; | ||||
| import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; | ||||
| import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; | ||||
| import { ObjectRecord } from '@/object-record/types/ObjectRecord'; | ||||
| import { RelationDefinitionType } from '~/generated-metadata/graphql'; | ||||
| @@ -107,12 +107,9 @@ const percentage = (part: number, whole: number): number => { | ||||
|   return Math.round((part / whole) * 100); | ||||
| }; | ||||
|  | ||||
| export const displayedExportProgress = ( | ||||
|   mode: 'all' | 'selection' = 'all', | ||||
|   progress?: ExportProgress, | ||||
| ): string => { | ||||
| export const displayedExportProgress = (progress?: ExportProgress): string => { | ||||
|   if (isUndefinedOrNull(progress?.exportedRecordCount)) { | ||||
|     return mode === 'all' ? 'Export View as CSV' : 'Export Selection as CSV'; | ||||
|     return 'Export'; | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|   | ||||
| @@ -0,0 +1,110 @@ | ||||
| import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||||
| import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; | ||||
| import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; | ||||
| import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; | ||||
| import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; | ||||
| import { ViewGroup } from '@/views/types/ViewGroup'; | ||||
| import { useRecoilCallback } from 'recoil'; | ||||
| import { v4 } from 'uuid'; | ||||
| import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; | ||||
|  | ||||
| type UseHandleRecordGroupFieldParams = { | ||||
|   viewBarComponentId: string; | ||||
| }; | ||||
|  | ||||
| export const useHandleRecordGroupField = ({ | ||||
|   viewBarComponentId, | ||||
| }: UseHandleRecordGroupFieldParams) => { | ||||
|   const { createViewGroupRecords, deleteViewGroupRecords } = | ||||
|     usePersistViewGroupRecords(); | ||||
|  | ||||
|   const currentViewIdCallbackState = useRecoilComponentCallbackStateV2( | ||||
|     currentViewIdComponentState, | ||||
|     viewBarComponentId, | ||||
|   ); | ||||
|  | ||||
|   const { getViewFromCache } = useGetViewFromCache(); | ||||
|  | ||||
|   const handleRecordGroupFieldChange = useRecoilCallback( | ||||
|     ({ snapshot }) => | ||||
|       async (fieldMetadataItem: FieldMetadataItem) => { | ||||
|         const currentViewId = snapshot | ||||
|           .getLoadable(currentViewIdCallbackState) | ||||
|           .getValue(); | ||||
|  | ||||
|         if (!currentViewId) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const view = await getViewFromCache(currentViewId); | ||||
|  | ||||
|         if (isUndefinedOrNull(view)) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if ( | ||||
|           isUndefinedOrNull(fieldMetadataItem.options) || | ||||
|           fieldMetadataItem.options.length === 0 | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const existingGroupKeys = new Set( | ||||
|           view.viewGroups.map( | ||||
|             (group) => `${group.fieldMetadataId}:${group.fieldValue}`, | ||||
|           ), | ||||
|         ); | ||||
|  | ||||
|         const viewGroupsToCreate = fieldMetadataItem.options | ||||
|           // Avoid creation of already existing view groups | ||||
|           .filter( | ||||
|             (option) => | ||||
|               !existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`), | ||||
|           ) | ||||
|           .map( | ||||
|             (option, index) => | ||||
|               ({ | ||||
|                 __typename: 'ViewGroup', | ||||
|                 id: v4(), | ||||
|                 fieldValue: option.value, | ||||
|                 isVisible: true, | ||||
|                 position: index, | ||||
|                 fieldMetadataId: fieldMetadataItem.id, | ||||
|               }) satisfies ViewGroup, | ||||
|           ); | ||||
|  | ||||
|         if (viewGroupsToCreate.length > 0) { | ||||
|           await createViewGroupRecords(viewGroupsToCreate, view); | ||||
|         } | ||||
|       }, | ||||
|     [createViewGroupRecords, currentViewIdCallbackState, getViewFromCache], | ||||
|   ); | ||||
|  | ||||
|   const resetRecordGroupField = useRecoilCallback( | ||||
|     ({ snapshot }) => | ||||
|       async () => { | ||||
|         const currentViewId = snapshot | ||||
|           .getLoadable(currentViewIdCallbackState) | ||||
|           .getValue(); | ||||
|  | ||||
|         if (!currentViewId) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const view = await getViewFromCache(currentViewId); | ||||
|  | ||||
|         if (isUndefinedOrNull(view)) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (view.viewGroups.length === 0) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         await deleteViewGroupRecords(view.viewGroups); | ||||
|       }, | ||||
|     [deleteViewGroupRecords, currentViewIdCallbackState, getViewFromCache], | ||||
|   ); | ||||
|  | ||||
|   return { handleRecordGroupFieldChange, resetRecordGroupField }; | ||||
| }; | ||||
| @@ -50,13 +50,13 @@ export const useLoadRecordIndexBoard = ({ | ||||
|     recordIndexViewFilterGroupsState, | ||||
|   ); | ||||
|  | ||||
|   const recordIndexGroupDefinitions = useRecoilComponentValueV2( | ||||
|   const recordGroupDefinitions = useRecoilComponentValueV2( | ||||
|     recordGroupDefinitionsComponentState, | ||||
|   ); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setColumns(recordIndexGroupDefinitions); | ||||
|   }, [recordIndexGroupDefinitions, setColumns]); | ||||
|     setColumns(recordGroupDefinitions); | ||||
|   }, [recordGroupDefinitions, setColumns]); | ||||
|  | ||||
|   const recordIndexFilters = useRecoilValue(recordIndexFiltersState); | ||||
|   const recordIndexSorts = useRecoilValue(recordIndexSortsState); | ||||
|   | ||||
| @@ -57,7 +57,9 @@ export const useFindManyParams = ( | ||||
|       ); | ||||
|  | ||||
|       if (!fieldMetadataItem) { | ||||
|         return {}; | ||||
|         throw new Error( | ||||
|           `Field metadata item with id ${currentRecordGroupDefinition.fieldMetadataId} not found`, | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton'; | ||||
| import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent'; | ||||
| import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; | ||||
| import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; | ||||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
|  | ||||
| type RecordIndexOptionsDropdownProps = { | ||||
|   viewType: ViewType; | ||||
|   objectMetadataItem: ObjectMetadataItem; | ||||
|   recordIndexId: string; | ||||
| }; | ||||
|  | ||||
| export const RecordIndexOptionsDropdown = ({ | ||||
|   recordIndexId, | ||||
|   objectMetadataItem, | ||||
|   viewType, | ||||
| }: RecordIndexOptionsDropdownProps) => { | ||||
|   return ( | ||||
|     <Dropdown | ||||
|       dropdownId={RECORD_INDEX_OPTIONS_DROPDOWN_ID} | ||||
|       clickableComponent={<RecordIndexOptionsDropdownButton />} | ||||
|       dropdownMenuWidth={'200px'} | ||||
|       dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }} | ||||
|       dropdownOffset={{ y: 8 }} | ||||
|       dropdownComponents={ | ||||
|         <RecordIndexOptionsDropdownContent | ||||
|           viewType={viewType} | ||||
|           objectMetadataItem={objectMetadataItem} | ||||
|           recordIndexId={recordIndexId} | ||||
|         /> | ||||
|       } | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,379 +0,0 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { Key } from 'ts-key-enum'; | ||||
| import { | ||||
|   IconBaselineDensitySmall, | ||||
|   IconChevronLeft, | ||||
|   IconEyeOff, | ||||
|   IconFileExport, | ||||
|   IconFileImport, | ||||
|   IconRotate2, | ||||
|   IconSettings, | ||||
|   IconTag, | ||||
|   MenuItem, | ||||
|   MenuItemNavigate, | ||||
|   MenuItemToggle, | ||||
|   UndecoratedLink, | ||||
|   useIcons, | ||||
| } from 'twenty-ui'; | ||||
|  | ||||
| import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; | ||||
| import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; | ||||
| import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; | ||||
|  | ||||
| import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; | ||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||
| import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; | ||||
| import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; | ||||
| import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; | ||||
| import { | ||||
|   displayedExportProgress, | ||||
|   useExportRecords, | ||||
| } from '@/object-record/record-index/export/hooks/useExportRecords'; | ||||
| import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; | ||||
| import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; | ||||
| import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; | ||||
| import { useOpenObjectRecordsSpreasheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog'; | ||||
| import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | ||||
| import { SettingsPath } from '@/types/SettingsPath'; | ||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||
| import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; | ||||
| import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; | ||||
| import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; | ||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||
| import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; | ||||
| import { ViewGroupsVisibilityDropdownSection } from '@/views/components/ViewGroupsVisibilityDropdownSection'; | ||||
| import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; | ||||
| import { ViewType } from '@/views/types/ViewType'; | ||||
| import { useLocation } from 'react-router-dom'; | ||||
| import { useSetRecoilState } from 'recoil'; | ||||
|  | ||||
| type RecordIndexOptionsMenu = | ||||
|   | 'viewGroups' | ||||
|   | 'hiddenViewGroups' | ||||
|   | 'fields' | ||||
|   | 'hiddenFields'; | ||||
|  | ||||
| type RecordIndexOptionsDropdownContentProps = { | ||||
|   recordIndexId: string; | ||||
|   objectMetadataItem: ObjectMetadataItem; | ||||
|   viewType: ViewType; | ||||
| }; | ||||
|  | ||||
| // TODO: Break this component down | ||||
| export const RecordIndexOptionsDropdownContent = ({ | ||||
|   viewType, | ||||
|   recordIndexId, | ||||
|   objectMetadataItem, | ||||
| }: RecordIndexOptionsDropdownContentProps) => { | ||||
|   const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); | ||||
|  | ||||
|   const { getIcon } = useIcons(); | ||||
|  | ||||
|   const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID); | ||||
|  | ||||
|   const [currentMenu, setCurrentMenu] = useState< | ||||
|     RecordIndexOptionsMenu | undefined | ||||
|   >(undefined); | ||||
|  | ||||
|   const resetMenu = () => setCurrentMenu(undefined); | ||||
|  | ||||
|   const handleSelectMenu = (option: RecordIndexOptionsMenu) => { | ||||
|     setCurrentMenu(option); | ||||
|   }; | ||||
|  | ||||
|   const { objectNamePlural } = useObjectNamePluralFromSingular({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|  | ||||
|   const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { | ||||
|     objectSlug: objectNamePlural, | ||||
|   }); | ||||
|  | ||||
|   useScopedHotkeys( | ||||
|     [Key.Escape], | ||||
|     () => { | ||||
|       closeDropdown(); | ||||
|     }, | ||||
|     TableOptionsHotkeyScope.Dropdown, | ||||
|   ); | ||||
|  | ||||
|   const { | ||||
|     handleColumnVisibilityChange, | ||||
|     handleReorderColumns, | ||||
|     visibleTableColumns, | ||||
|     hiddenTableColumns, | ||||
|   } = useRecordIndexOptionsForTable(recordIndexId); | ||||
|  | ||||
|   const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } = | ||||
|     useHandleToggleTrashColumnFilter({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   const { | ||||
|     visibleBoardFields, | ||||
|     hiddenBoardFields, | ||||
|     handleReorderBoardFields, | ||||
|     handleBoardFieldVisibilityChange, | ||||
|     isCompactModeActive, | ||||
|     setAndPersistIsCompactModeActive, | ||||
|   } = useRecordIndexOptionsForBoard({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|     recordBoardId: recordIndexId, | ||||
|     viewBarId: recordIndexId, | ||||
|   }); | ||||
|  | ||||
|   const { | ||||
|     hiddenRecordGroups, | ||||
|     visibleRecordGroups, | ||||
|     viewGroupFieldMetadataItem, | ||||
|   } = useRecordGroups({ | ||||
|     objectNameSingular: objectMetadataItem.nameSingular, | ||||
|   }); | ||||
|   const { handleVisibilityChange: handleRecordGroupVisibilityChange } = | ||||
|     useRecordGroupVisibility({ | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|   const { handleOrderChange: handleRecordGroupOrderChange } = | ||||
|     useRecordGroupReorder({ | ||||
|       objectNameSingular: objectMetadataItem.nameSingular, | ||||
|       viewBarId: recordIndexId, | ||||
|     }); | ||||
|  | ||||
|   const viewGroupSettingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { | ||||
|     id: viewGroupFieldMetadataItem?.name, | ||||
|     objectSlug: objectNamePlural, | ||||
|   }); | ||||
|  | ||||
|   const visibleRecordFields = | ||||
|     viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns; | ||||
|  | ||||
|   const hiddenRecordFields = | ||||
|     viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns; | ||||
|  | ||||
|   const handleReorderFields = | ||||
|     viewType === ViewType.Kanban | ||||
|       ? handleReorderBoardFields | ||||
|       : handleReorderColumns; | ||||
|  | ||||
|   const handleChangeFieldVisibility = | ||||
|     viewType === ViewType.Kanban | ||||
|       ? handleBoardFieldVisibilityChange | ||||
|       : handleColumnVisibilityChange; | ||||
|  | ||||
|   const { openObjectRecordsSpreasheetImportDialog } = | ||||
|     useOpenObjectRecordsSpreasheetImportDialog(objectMetadataItem.nameSingular); | ||||
|  | ||||
|   const { progress, download } = useExportRecords({ | ||||
|     delayMs: 100, | ||||
|     filename: `${objectMetadataItem.nameSingular}.csv`, | ||||
|     objectMetadataItem, | ||||
|     recordIndexId, | ||||
|     viewType, | ||||
|   }); | ||||
|  | ||||
|   const location = useLocation(); | ||||
|   const setNavigationMemorizedUrl = useSetRecoilState( | ||||
|     navigationMemorizedUrlState, | ||||
|   ); | ||||
|  | ||||
|   const isViewGroupMenuItemVisible = | ||||
|     viewGroupFieldMetadataItem && | ||||
|     (visibleRecordGroups.length > 0 || hiddenRecordGroups.length > 0); | ||||
|  | ||||
|   const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( | ||||
|     contextStoreNumberOfSelectedRecordsComponentState, | ||||
|   ); | ||||
|  | ||||
|   const mode = contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all'; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (currentMenu === 'hiddenViewGroups' && hiddenRecordGroups.length === 0) { | ||||
|       setCurrentMenu('viewGroups'); | ||||
|     } | ||||
|   }, [hiddenRecordGroups, currentMenu]); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {!currentMenu && ( | ||||
|         <DropdownMenuItemsContainer> | ||||
|           {isViewGroupMenuItemVisible && ( | ||||
|             <MenuItem | ||||
|               onClick={() => handleSelectMenu('viewGroups')} | ||||
|               LeftIcon={getIcon(currentViewWithCombinedFiltersAndSorts?.icon)} | ||||
|               text={viewGroupFieldMetadataItem.label} | ||||
|               hasSubMenu | ||||
|             /> | ||||
|           )} | ||||
|           <MenuItem | ||||
|             onClick={() => handleSelectMenu('fields')} | ||||
|             LeftIcon={IconTag} | ||||
|             text="Fields" | ||||
|             hasSubMenu | ||||
|           /> | ||||
|           <MenuItem | ||||
|             onClick={() => openObjectRecordsSpreasheetImportDialog()} | ||||
|             LeftIcon={IconFileImport} | ||||
|             text="Import" | ||||
|           /> | ||||
|           <MenuItem | ||||
|             onClick={download} | ||||
|             LeftIcon={IconFileExport} | ||||
|             text={displayedExportProgress(mode, progress)} | ||||
|           /> | ||||
|           <MenuItem | ||||
|             onClick={() => { | ||||
|               handleToggleTrashColumnFilter(); | ||||
|               toggleSoftDeleteFilterState(true); | ||||
|               closeDropdown(); | ||||
|             }} | ||||
|             LeftIcon={IconRotate2} | ||||
|             text={`Deleted ${objectNamePlural}`} | ||||
|           /> | ||||
|         </DropdownMenuItemsContainer> | ||||
|       )} | ||||
|       {currentMenu === 'viewGroups' && ( | ||||
|         <> | ||||
|           <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}> | ||||
|             {viewGroupFieldMetadataItem?.label} | ||||
|           </DropdownMenuHeader> | ||||
|           <ViewGroupsVisibilityDropdownSection | ||||
|             title={viewGroupFieldMetadataItem?.label ?? ''} | ||||
|             viewGroups={visibleRecordGroups} | ||||
|             onDragEnd={handleRecordGroupOrderChange} | ||||
|             onVisibilityChange={handleRecordGroupVisibilityChange} | ||||
|             isDraggable | ||||
|             showSubheader={false} | ||||
|             showDragGrip={true} | ||||
|           /> | ||||
|           {hiddenRecordGroups.length > 0 && ( | ||||
|             <> | ||||
|               <DropdownMenuSeparator /> | ||||
|               <DropdownMenuItemsContainer> | ||||
|                 <MenuItemNavigate | ||||
|                   onClick={() => handleSelectMenu('hiddenViewGroups')} | ||||
|                   LeftIcon={IconEyeOff} | ||||
|                   text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`} | ||||
|                 /> | ||||
|               </DropdownMenuItemsContainer> | ||||
|             </> | ||||
|           )} | ||||
|         </> | ||||
|       )} | ||||
|       {currentMenu === 'fields' && ( | ||||
|         <> | ||||
|           <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}> | ||||
|             Fields | ||||
|           </DropdownMenuHeader> | ||||
|           <ScrollWrapper contextProviderName="dropdownMenuItemsContainer"> | ||||
|             <ViewFieldsVisibilityDropdownSection | ||||
|               title="Visible" | ||||
|               fields={visibleRecordFields} | ||||
|               isDraggable | ||||
|               onDragEnd={handleReorderFields} | ||||
|               onVisibilityChange={handleChangeFieldVisibility} | ||||
|               showSubheader={false} | ||||
|               showDragGrip={true} | ||||
|             /> | ||||
|           </ScrollWrapper> | ||||
|           <DropdownMenuSeparator /> | ||||
|           <DropdownMenuItemsContainer> | ||||
|             <MenuItemNavigate | ||||
|               onClick={() => handleSelectMenu('hiddenFields')} | ||||
|               LeftIcon={IconEyeOff} | ||||
|               text="Hidden Fields" | ||||
|             /> | ||||
|           </DropdownMenuItemsContainer> | ||||
|         </> | ||||
|       )} | ||||
|       {currentMenu === 'hiddenViewGroups' && ( | ||||
|         <> | ||||
|           <DropdownMenuHeader | ||||
|             StartIcon={IconChevronLeft} | ||||
|             onClick={() => setCurrentMenu('viewGroups')} | ||||
|           > | ||||
|             Hidden {viewGroupFieldMetadataItem?.label} | ||||
|           </DropdownMenuHeader> | ||||
|           <ViewGroupsVisibilityDropdownSection | ||||
|             title={`Hidden ${viewGroupFieldMetadataItem?.label}`} | ||||
|             viewGroups={hiddenRecordGroups} | ||||
|             onVisibilityChange={handleRecordGroupVisibilityChange} | ||||
|             isDraggable={false} | ||||
|             showSubheader={false} | ||||
|             showDragGrip={false} | ||||
|           /> | ||||
|           <DropdownMenuSeparator /> | ||||
|           <UndecoratedLink | ||||
|             to={viewGroupSettingsUrl} | ||||
|             onClick={() => { | ||||
|               setNavigationMemorizedUrl(location.pathname + location.search); | ||||
|               closeDropdown(); | ||||
|             }} | ||||
|           > | ||||
|             <DropdownMenuItemsContainer> | ||||
|               <MenuItem LeftIcon={IconSettings} text="Edit field values" /> | ||||
|             </DropdownMenuItemsContainer> | ||||
|           </UndecoratedLink> | ||||
|         </> | ||||
|       )} | ||||
|       {currentMenu === 'hiddenFields' && ( | ||||
|         <> | ||||
|           <DropdownMenuHeader | ||||
|             StartIcon={IconChevronLeft} | ||||
|             onClick={() => setCurrentMenu('fields')} | ||||
|           > | ||||
|             Hidden Fields | ||||
|           </DropdownMenuHeader> | ||||
|           {hiddenRecordFields.length > 0 && ( | ||||
|             <ScrollWrapper contextProviderName="dropdownMenuItemsContainer"> | ||||
|               <ViewFieldsVisibilityDropdownSection | ||||
|                 title="Hidden" | ||||
|                 fields={hiddenRecordFields} | ||||
|                 isDraggable={false} | ||||
|                 onVisibilityChange={handleChangeFieldVisibility} | ||||
|                 showSubheader={false} | ||||
|                 showDragGrip={false} | ||||
|               /> | ||||
|             </ScrollWrapper> | ||||
|           )} | ||||
|           <DropdownMenuSeparator /> | ||||
|  | ||||
|           <UndecoratedLink | ||||
|             to={settingsUrl} | ||||
|             onClick={() => { | ||||
|               setNavigationMemorizedUrl(location.pathname + location.search); | ||||
|               closeDropdown(); | ||||
|             }} | ||||
|           > | ||||
|             <DropdownMenuItemsContainer> | ||||
|               <MenuItem LeftIcon={IconSettings} text="Edit Fields" /> | ||||
|             </DropdownMenuItemsContainer> | ||||
|           </UndecoratedLink> | ||||
|         </> | ||||
|       )} | ||||
|  | ||||
|       {viewType === ViewType.Kanban && !currentMenu && ( | ||||
|         <> | ||||
|           <DropdownMenuSeparator /> | ||||
|           <DropdownMenuItemsContainer> | ||||
|             <MenuItemToggle | ||||
|               LeftIcon={IconBaselineDensitySmall} | ||||
|               onToggleChange={() => | ||||
|                 setAndPersistIsCompactModeActive( | ||||
|                   !isCompactModeActive, | ||||
|                   currentViewWithCombinedFiltersAndSorts, | ||||
|                 ) | ||||
|               } | ||||
|               toggled={isCompactModeActive} | ||||
|               text="Compact view" | ||||
|               toggleSize="small" | ||||
|             /> | ||||
|           </DropdownMenuItemsContainer> | ||||
|         </> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,2 +0,0 @@ | ||||
| export const RECORD_INDEX_BOARD_OPTIONS_DROPDOWN_ID = | ||||
|   'record-index-table-options-dropdown-id'; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Weiko
					Weiko