diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index e334be495..0d3840221 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -330,7 +330,6 @@ export enum FeatureFlagKey { IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', - IsViewGroupsEnabled = 'IsViewGroupsEnabled', IsWorkflowEnabled = 'IsWorkflowEnabled' } diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts index bb650cad7..5dd85d297 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts @@ -32,7 +32,7 @@ export const triggerAttachRelationOptimisticEffect = ({ id: targetRecordCacheId, fields: { [fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => { - const fieldValueisObjectRecordConnectionWithRefs = + const fieldValueIsObjectRecordConnectionWithRefs = isObjectRecordConnectionWithRefs( sourceObjectNameSingular, targetRecordFieldValue, @@ -47,7 +47,7 @@ export const triggerAttachRelationOptimisticEffect = ({ return targetRecordFieldValue; } - if (fieldValueisObjectRecordConnectionWithRefs) { + if (fieldValueIsObjectRecordConnectionWithRefs) { const nextEdges: RecordGqlRefEdge[] = [ ...targetRecordFieldValue.edges, { diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx index f051fa4e3..06d39bf6a 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx @@ -34,6 +34,7 @@ export const ObjectOptionsDropdown = ({ clickableComponent={ Options } + onClose={handleResetContent} dropdownComponents={ { const { + viewType, currentContentId, recordIndexId, objectMetadataItem, @@ -47,6 +48,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); const viewGroupSettingsUrl = getSettingsPagePath( diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx index 62b06acd7..a739c457b 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx @@ -30,8 +30,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { ViewType } from '@/views/types/ViewType'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; export const ObjectOptionsDropdownMenuContent = () => { const { @@ -42,10 +41,6 @@ export const ObjectOptionsDropdownMenuContent = () => { closeDropdown, } = useOptionsDropdown(); - const isViewGroupEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsViewGroupsEnabled, - ); - const { getIcon } = useIcons(); const { currentViewWithCombinedFiltersAndSorts: currentView } = useGetCurrentView(); @@ -120,9 +115,13 @@ export const ObjectOptionsDropdownMenuContent = () => { contextualText={`${visibleBoardFields.length} shown`} hasSubMenu /> - {(viewType === ViewType.Kanban || isViewGroupEnabled) && ( + {viewType === ViewType.Kanban && currentView?.key !== 'INDEX' && ( onContentChange('recordGroups')} + onClick={() => + isDefined(recordGroupFieldMetadata) + ? onContentChange('recordGroups') + : onContentChange('recordGroupFields') + } LeftIcon={IconLayoutList} text="Group by" contextualText={recordGroupFieldMetadata?.label} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index d5fa0be68..3c0a0f9bf 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -26,6 +26,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { @@ -36,6 +37,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { recordIndexId, objectMetadataItem, onContentChange, + resetContent, closeDropdown, } = useOptionsDropdown(); @@ -47,7 +49,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { hiddenRecordGroupIdsComponentSelector, ); - const recordGroupFieldMetadataItem = useRecoilComponentValueV2( + const recordGroupFieldMetadata = useRecoilComponentValueV2( recordGroupFieldMetadataComponentState, ); @@ -64,11 +66,14 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { viewBarComponentId: recordIndexId, }); - const newFieldSettingsUrl = getSettingsPagePath( - SettingsPath.ObjectNewFieldSelect, + const newSelectFieldSettingsUrl = getSettingsPagePath( + SettingsPath.ObjectNewFieldConfigure, { objectSlug: objectNamePlural, }, + { + fieldType: FieldMetadataType.Select, + }, ); const location = useLocation(); @@ -101,7 +106,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { <> onContentChange('recordGroups')} + onClick={() => + isDefined(recordGroupFieldMetadata) + ? onContentChange('recordGroups') + : resetContent() + } > Group by @@ -114,13 +123,13 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { {filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => ( handleRecordGroupFieldChange(fieldMetadataItem)} LeftIcon={getIcon(fieldMetadataItem.icon)} text={fieldMetadataItem.label} @@ -130,7 +139,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { { setNavigationMemorizedUrl(location.pathname + location.search); closeDropdown(); diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx index c4b6d05f1..488233469 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx @@ -17,8 +17,7 @@ import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const ObjectOptionsDropdownRecordGroupSortContent = () => { - const { currentContentId, onContentChange, closeDropdown } = - useOptionsDropdown(); + const { currentContentId, onContentChange } = useOptionsDropdown(); const hiddenRecordGroupIds = useRecoilComponentValueV2( hiddenRecordGroupIdsComponentSelector, @@ -30,7 +29,6 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => { const handleRecordGroupSortChange = (sort: RecordGroupSort) => { setRecordGroupSort(sort); - closeDropdown(); }; useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx index 32c6eedeb..e1fab350f 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx @@ -11,47 +11,54 @@ import { } from 'twenty-ui'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; +import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; -import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; +import { useRecordGroupReorderConfirmationModal } from '@/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; -import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; -import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; export const ObjectOptionsDropdownRecordGroupsContent = () => { - const isViewGroupEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsViewGroupsEnabled, - ); + const { + viewType, + currentContentId, + recordIndexId, + onContentChange, + resetContent, + } = useOptionsDropdown(); - const { currentContentId, recordIndexId, onContentChange, resetContent } = - useOptionsDropdown(); + const { currentViewWithCombinedFiltersAndSorts: currentView } = + useGetCurrentView(); const recordGroupFieldMetadata = useRecoilComponentValueV2( recordGroupFieldMetadataComponentState, ); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + viewType, ); const hiddenRecordGroupIds = useRecoilComponentValueV2( hiddenRecordGroupIdsComponentSelector, ); - const isDragableSortRecordGroup = useRecoilComponentValueV2( - recordIndexRecordGroupIsDraggableSortComponentSelector, + const hideEmptyRecordGroup = useRecoilComponentFamilyValueV2( + recordIndexRecordGroupHideComponentFamilyState, + viewType, ); - const hideEmptyRecordGroup = useRecoilComponentValueV2( - recordIndexRecordGroupHideComponentState, + const recordGroupSort = useRecoilComponentValueV2( + recordIndexRecordGroupSortComponentState, ); const { @@ -59,12 +66,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { handleHideEmptyRecordGroupChange, } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); - const { handleOrderChange: handleRecordGroupOrderChange } = - useRecordGroupReorder({ - viewBarId: recordIndexId, - }); + const { + handleRecordGroupOrderChangeWithModal, + handleRecordGroupReorderConfirmClick, + } = useRecordGroupReorderConfirmationModal({ + recordIndexId, + viewType, + }); useEffect(() => { if ( @@ -81,22 +92,20 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { Group by - {isViewGroupEnabled && ( + {currentView?.key !== 'INDEX' && ( <> onContentChange('recordGroupFields')} LeftIcon={IconLayoutList} - text={ - !recordGroupFieldMetadata - ? 'Group by' - : `Group by "${recordGroupFieldMetadata.label}"` - } + text="Group by" + contextualText={recordGroupFieldMetadata?.label} hasSubMenu /> onContentChange('recordGroupSort')} LeftIcon={IconSortDescending} text="Sort" + contextualText={recordGroupSort} hasSubMenu /> @@ -115,9 +124,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { @@ -134,6 +143,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 88c253ba3..3049897e5 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -15,7 +15,7 @@ import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoar import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -26,8 +26,9 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { ViewType } from '@/views/types/ViewType'; import { useScrollRestoration } from '~/hooks/useScrollRestoration'; const StyledContainer = styled.div` @@ -64,8 +65,9 @@ export const RecordBoard = () => { useContext(RecordBoardContext); const boardRef = useRef(null); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); const recordIndexRecordIdsByGroupFamilyState = diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index e62c65811..3ca0b6e68 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -1,6 +1,7 @@ import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { ViewType } from '@/views/types/ViewType'; import styled from '@emotion/styled'; const StyledHeaderContainer = styled.div` @@ -23,8 +24,9 @@ const StyledHeaderContainer = styled.div` `; export const RecordBoardHeader = () => { - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts index 60916cfea..63b127408 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts @@ -2,18 +2,19 @@ import { useRecoilCallback } from 'recoil'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { ViewType } from '@/views/types/ViewType'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { - const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentFamilySelector, ); const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( @@ -32,7 +33,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { (records: ObjectRecord[]) => { const recordGroupIds = getSnapshotValue( snapshot, - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector(ViewType.Kanban), ); for (const recordGroupId of recordGroupIds) { @@ -72,7 +73,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { } }, [ - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector, recordIndexRecordIdsByGroupFamilyState, recordGroupFieldMetadataState, ], diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx index 5e096dc02..bcf0c98e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx @@ -4,9 +4,10 @@ import { useCallback, useRef } from 'react'; import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions'; 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 { MenuItem } from 'twenty-ui'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { ViewType } from '@/views/types/ViewType'; +import { MenuItem } from 'twenty-ui'; const StyledMenuContainer = styled.div` position: absolute; @@ -27,7 +28,9 @@ export const RecordBoardColumnDropdownMenu = ({ }: RecordBoardColumnDropdownMenuProps) => { const boardColumnMenuRef = useRef(null); - const recordGroupActions = useRecordGroupActions(); + const recordGroupActions = useRecordGroupActions({ + viewType: ViewType.Kanban, + }); const closeMenu = useCallback(() => { onClose(); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx new file mode 100644 index 000000000..0a906ec1a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx @@ -0,0 +1,39 @@ +import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { createPortal } from 'react-dom'; +import { useRecoilState } from 'recoil'; + +type RecordGroupReorderConfirmationModalProps = { + onConfirmClick: () => void; +}; + +export const RecordGroupReorderConfirmationModal = ({ + onConfirmClick, +}: RecordGroupReorderConfirmationModalProps) => { + const [ + isRecordGroupReorderConfirmationModalVisible, + setIsRecordGroupReorderConfirmationModalVisible, + ] = useRecoilState(isRecordGroupReorderConfirmationModalVisibleState); + + const recordGroupSort = useRecoilComponentValueV2( + recordIndexRecordGroupSortComponentState, + ); + + if (!isRecordGroupReorderConfirmationModalVisible) { + return null; + } + + return createPortal( + , + document.body, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts index 2c88b9609..6121f944b 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts @@ -8,12 +8,19 @@ import { RecordGroupAction } from '@/object-record/record-group/types/RecordGrou import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +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(); @@ -34,6 +41,7 @@ export const useRecordGroupActions = () => { const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); const setNavigationMemorizedUrl = useSetRecoilState( diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts index 9b1c038e1..93e1f05c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts @@ -2,11 +2,12 @@ import { OnDragEndResponder } from '@hello-pangea/dnd'; import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; 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'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; @@ -15,15 +16,17 @@ import { isDefined } from '~/utils/isDefined'; type UseRecordGroupHandlersParams = { viewBarId: string; + viewType: ViewType; }; export const useRecordGroupReorder = ({ viewBarId, + viewType, }: UseRecordGroupHandlersParams) => { const setRecordGroup = useSetRecordGroup(viewBarId); - const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentFamilySelector, ); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); @@ -37,7 +40,7 @@ export const useRecordGroupReorder = ({ const visibleRecordGroupIds = getSnapshotValue( snapshot, - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector(viewType), ); const reorderedVisibleRecordGroupIds = moveArrayItem( @@ -80,7 +83,12 @@ export const useRecordGroupReorder = ({ mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), ); }, - [saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector], + [ + saveViewGroups, + setRecordGroup, + viewType, + visibleRecordGroupIdsFamilySelector, + ], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts new file mode 100644 index 000000000..2831cc1f5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts @@ -0,0 +1,78 @@ +import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; +import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState'; +import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; +import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { ViewType } from '@/views/types/ViewType'; +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; + +type UseRecordGroupReorderConfirmationModalParams = { + recordIndexId: string; + viewType: ViewType; +}; + +export const useRecordGroupReorderConfirmationModal = ({ + recordIndexId, + viewType, +}: UseRecordGroupReorderConfirmationModalParams) => { + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + const { goBackToPreviousDropdownFocusId } = + useGoBackToPreviousDropdownFocusId(); + + const setIsRecordGroupReorderConfirmationModalVisible = useSetRecoilState( + isRecordGroupReorderConfirmationModalVisibleState, + ); + + const [pendingDragEndReorder, setPendingDragEndReorder] = + useState | null>(null); + + const { handleOrderChange: handleRecordGroupOrderChange } = + useRecordGroupReorder({ + viewBarId: recordIndexId, + viewType, + }); + + const isDragableSortRecordGroup = useRecoilComponentValueV2( + recordIndexRecordGroupIsDraggableSortComponentSelector, + ); + + const setRecordGroupSort = useSetRecoilComponentStateV2( + recordIndexRecordGroupSortComponentState, + ); + + const handleRecordGroupOrderChangeWithModal: OnDragEndResponder = ( + result, + provided, + ) => { + if (!isDragableSortRecordGroup) { + setIsRecordGroupReorderConfirmationModalVisible(true); + setActiveDropdownFocusIdAndMemorizePrevious(null); + setPendingDragEndReorder([result, provided]); + } else { + handleRecordGroupOrderChange(result, provided); + } + }; + + const handleConfirmClick = () => { + if (!pendingDragEndReorder) { + throw new Error('pendingDragEndReorder is not set'); + } + + setRecordGroupSort(RecordGroupSort.Manual); + setPendingDragEndReorder(null); + handleRecordGroupOrderChange(...pendingDragEndReorder); + goBackToPreviousDropdownFocusId(); + }; + + return { + handleRecordGroupOrderChangeWithModal, + handleRecordGroupReorderConfirmClick: handleConfirmClick, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts index 405d637e2..b006459a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts @@ -1,20 +1,25 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; +import { ViewType } from '@/views/types/ViewType'; import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; import { useRecoilCallback } from 'recoil'; type UseRecordGroupVisibilityParams = { viewBarId: string; + viewType: ViewType; }; export const useRecordGroupVisibility = ({ viewBarId, + viewType, }: UseRecordGroupVisibilityParams) => { - const objectOptionsDropdownRecordGroupHideState = - useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); + const objectOptionsDropdownRecordGroupHideFamilyState = + useRecoilComponentCallbackStateV2( + recordIndexRecordGroupHideComponentFamilyState, + ); const { saveViewGroup } = useSaveCurrentViewGroups(viewBarId); @@ -27,22 +32,19 @@ export const useRecordGroupVisibility = ({ ); saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup)); - - // If visibility is manually toggled, we should reset the hideEmptyRecordGroup state - set(objectOptionsDropdownRecordGroupHideState, false); }, - [saveViewGroup, objectOptionsDropdownRecordGroupHideState], + [saveViewGroup], ); const handleHideEmptyRecordGroupChange = useRecoilCallback( ({ set }) => async () => { set( - objectOptionsDropdownRecordGroupHideState, + objectOptionsDropdownRecordGroupHideFamilyState(viewType), (currentHideState) => !currentHideState, ); }, - [objectOptionsDropdownRecordGroupHideState], + [viewType, objectOptionsDropdownRecordGroupHideFamilyState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts new file mode 100644 index 000000000..3f0d0c047 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isRecordGroupReorderConfirmationModalVisibleState = atom({ + key: 'isRecordGroupReorderConfirmationModalVisibleState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts new file mode 100644 index 000000000..e3ca2769f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts @@ -0,0 +1,8 @@ +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { atom } from 'recoil'; + +export const recordGroupPendingDragEndReorderState = + atom | null>({ + key: 'recordGroupPendingDragEndReorderState', + default: null, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000..e500f1234 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts @@ -0,0 +1,50 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; + +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const availableRecordGroupIdsComponentSelector = + createComponentSelectorV2({ + key: 'availableRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + const result: RecordGroupDefinition[] = []; + + for (const recordGroupId of recordGroupIds) { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + continue; + } + + if ( + recordGroupDefinition.type === RecordGroupDefinitionType.NoValue + ) { + continue; + } + + recordGroupSortedInsert(result, recordGroupDefinition, (a, b) => + a.title.localeCompare(b.title), + ); + } + + return result.map(({ id }) => id); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts new file mode 100644 index 000000000..139e3806b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts @@ -0,0 +1,84 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; +import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2'; + +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { ViewType } from '@/views/types/ViewType'; +import { isDefined } from '~/utils/isDefined'; + +export const visibleRecordGroupIdsComponentFamilySelector = + createComponentFamilySelectorV2({ + key: 'visibleRecordGroupIdsComponentFamilySelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId, familyKey }) => + ({ get }) => { + const recordGroupSort = get( + recordIndexRecordGroupSortComponentState.atomFamily({ + instanceId, + }), + ); + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + const hideEmptyRecordGroup = get( + recordIndexRecordGroupHideComponentFamilyState.atomFamily({ + instanceId, + familyKey, + }), + ); + + const result: RecordGroupDefinition[] = []; + + const comparator = ( + a: RecordGroupDefinition, + b: RecordGroupDefinition, + ) => { + switch (recordGroupSort) { + case RecordGroupSort.Alphabetical: + return a.title.localeCompare(b.title); + case RecordGroupSort.ReverseAlphabetical: + return b.title.localeCompare(a.title); + case RecordGroupSort.Manual: + default: + return a.position - b.position; + } + }; + + for (const recordGroupId of recordGroupIds) { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + const recordIds = get( + recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({ + instanceId, + familyKey: recordGroupId, + }), + ); + + if (!isDefined(recordGroupDefinition)) { + continue; + } + + if (hideEmptyRecordGroup && recordIds.length === 0) { + continue; + } + + if (!recordGroupDefinition.isVisible) { + continue; + } + + recordGroupSortedInsert(result, recordGroupDefinition, comparator); + } + + return result.map(({ id }) => id); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts deleted file mode 100644 index abd766868..000000000 --- a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; -import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; -import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; -import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; - -import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { isDefined } from '~/utils/isDefined'; - -export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2< - RecordGroupDefinition['id'][] ->({ - key: 'visibleRecordGroupIdsComponentSelector', - componentInstanceContext: ViewComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => { - const recordGroupSort = get( - recordIndexRecordGroupSortComponentState.atomFamily({ - instanceId, - }), - ); - const recordGroupIds = get( - recordGroupIdsComponentState.atomFamily({ - instanceId, - }), - ); - const hideEmptyRecordGroup = get( - recordIndexRecordGroupHideComponentState.atomFamily({ - instanceId, - }), - ); - - const result: RecordGroupDefinition[] = []; - - const comparator = ( - a: RecordGroupDefinition, - b: RecordGroupDefinition, - ) => { - switch (recordGroupSort) { - case RecordGroupSort.Alphabetical: - return a.title.localeCompare(b.title); - case RecordGroupSort.ReverseAlphabetical: - return b.title.localeCompare(a.title); - case RecordGroupSort.Manual: - default: - return a.position - b.position; - } - }; - - for (const recordGroupId of recordGroupIds) { - const recordGroupDefinition = get( - recordGroupDefinitionFamilyState(recordGroupId), - ); - const recordIds = get( - recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({ - instanceId, - familyKey: recordGroupId, - }), - ); - - if (!isDefined(recordGroupDefinition)) { - continue; - } - - if (hideEmptyRecordGroup && recordIds.length === 0) { - continue; - } - - if (!recordGroupDefinition.isVisible) { - continue; - } - - recordGroupSortedInsert(result, recordGroupDefinition, comparator); - } - - return result.map(({ id }) => id); - }, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx similarity index 74% rename from packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx index c40959b1f..cd310e58e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx @@ -1,5 +1,5 @@ import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { availableRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; @@ -8,18 +8,27 @@ import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilCallback } from 'recoil'; -export const RecordIndexPageTableAddButtonInGroup = () => { - const dropdownId = `record-index-page-table-add-button-dropdown`; +type RecordIndexAddRecordInGroupDropdownProps = { + dropdownId: string; + clickableComponent: React.ReactNode; +}; +export const RecordIndexAddRecordInGroupDropdown = ({ + dropdownId, + clickableComponent, +}: RecordIndexAddRecordInGroupDropdownProps) => { const { objectMetadataItem } = useRecordIndexContextOrThrow(); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + + const recordGroupIds = useRecoilComponentValueV2( + availableRecordGroupIdsComponentSelector, ); const recordGroupFieldMetadata = useRecoilComponentValueV2( @@ -44,11 +53,13 @@ export const RecordIndexPageTableAddButtonInGroup = () => { (recordGroup: RecordGroupDefinition) => { set(isRecordGroupTableSectionToggledState(recordGroup.id), true); createNewTableRecordInGroup(recordGroup.id); + setActiveDropdownFocusIdAndMemorizePrevious(null); closeDropdown(); }, [ closeDropdown, createNewTableRecordInGroup, + setActiveDropdownFocusIdAndMemorizePrevious, isRecordGroupTableSectionToggledState, ], ); @@ -61,11 +72,11 @@ export const RecordIndexPageTableAddButtonInGroup = () => { } + clickableComponent={clickableComponent} dropdownId={dropdownId} dropdownComponents={ - {visibleRecordGroupIds.map((recordGroupId) => ( + {recordGroupIds.map((recordGroupId) => ( { const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow(); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); const recordIndexKanbanFieldMetadataId = useRecoilValue( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx index fe169ffe7..aef0ef97a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx @@ -1,6 +1,7 @@ import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; -import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup'; +import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown'; import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordIndexPageTableAddButton = () => { @@ -12,5 +13,10 @@ export const RecordIndexPageTableAddButton = () => { return ; } - return ; + return ( + } + /> + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts index 84cd0ff54..51d8d0179 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts @@ -61,6 +61,8 @@ export const useHandleRecordGroupField = ({ (option) => !existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`), ) + // Alphabetically sort the options by default + .sort((a, b) => a.value.localeCompare(b.value)) .map( (option, index) => ({ diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts new file mode 100644 index 000000000..da9bed69c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts @@ -0,0 +1,19 @@ +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { ViewType } from '@/views/types/ViewType'; + +export const recordIndexRecordGroupHideComponentFamilyState = + createComponentFamilyStateV2({ + key: 'recordIndexRecordGroupHideComponentFamilyState', + defaultValue: ({ familyKey }) => { + switch (familyKey) { + case ViewType.Kanban: + return false; + case ViewType.Table: + return true; + default: + return false; + } + }, + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts deleted file mode 100644 index 3500e331e..000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; - -export const recordIndexRecordGroupHideComponentState = - createComponentStateV2({ - key: 'recordIndexRecordGroupHideComponentState', - defaultValue: false, - componentInstanceContext: ViewComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 12dc2d83a..571510fcb 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { isNonEmptyString } from '@sniptt/guards'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; @@ -16,7 +16,7 @@ import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; +import { hasPendingRecordComponentSelector } from '@/object-record/record-table/states/selectors/hasPendingRecordComponentSelector'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -54,8 +54,8 @@ export const RecordTable = () => { recordTableId, ); - const pendingRecordId = useRecoilComponentValueV2( - recordTablePendingRecordIdComponentState, + const hasPendingRecord = useRecoilComponentValueV2( + hasPendingRecordComponentSelector, recordTableId, ); @@ -67,7 +67,7 @@ export const RecordTable = () => { const recordTableIsEmpty = !isRecordTableInitialLoading && allRecordIds.length === 0 && - isNull(pendingRecordId); + !hasPendingRecord; const { resetTableRowSelection, setRowSelected } = useRecordTable({ recordTableId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx index 680ab7b40..5d9619a53 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx @@ -1,6 +1,8 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; -import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll'; +import { RecordTableEmptyStateByGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll'; +import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll'; import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; @@ -11,6 +13,10 @@ export const RecordTableEmptyState = () => { const { recordTableId, objectNameSingular, objectMetadataItem } = useRecordTableContextOrThrow(); + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupsComponentSelector, + ); + const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const noRecordAtAll = totalCount === 0; @@ -26,7 +32,11 @@ export const RecordTableEmptyState = () => { } else if (isSoftDeleteActive === true) { return ; } else if (noRecordAtAll) { - return ; + if (hasRecordGroups) { + return ; + } + + return ; } else { return ; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx new file mode 100644 index 000000000..8259f8172 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx @@ -0,0 +1,52 @@ +import { Button, IconPlus } from 'twenty-ui'; + +import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; +import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; +import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; +import { ViewType } from '@/views/types/ViewType'; + +export const RecordTableEmptyStateByGroupNoRecordAtAll = () => { + const { objectMetadataItem } = useRecordTableContextOrThrow(); + + const setHideEmptyRecordGroup = useSetRecoilComponentFamilyStateV2( + recordIndexRecordGroupHideComponentFamilyState, + ViewType.Table, + ); + + const objectLabel = useObjectLabel(objectMetadataItem); + + const buttonTitle = `Add a ${objectLabel}`; + + const title = `Add your first ${objectLabel}`; + + const subTitle = `Use our API or add your first ${objectLabel} manually`; + + const handleButtonClick = () => { + // When we have no records in the group, we want to show the empty state + setHideEmptyRecordGroup(false); + }; + + return ( + + } + /> + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 8b12987a0..58abce2f2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -11,41 +11,49 @@ import { IconComponent, } from 'twenty-ui'; -type RecordTableEmptyStateDisplayProps = { - animatedPlaceholderType: AnimatedPlaceholderType; - title: string; - subTitle: string; - Icon: IconComponent; +type RecordTableEmptyStateDisplayButtonComponentProps = { + buttonComponent?: React.ReactNode; +}; + +type RecordTableEmptyStateDisplayButtonProps = { + ButtonIcon: IconComponent; buttonTitle: string; onClick: () => void; }; -export const RecordTableEmptyStateDisplay = ({ - Icon, - animatedPlaceholderType, - buttonTitle, - onClick, - subTitle, - title, -}: RecordTableEmptyStateDisplayProps) => { +type RecordTableEmptyStateDisplayProps = { + animatedPlaceholderType: AnimatedPlaceholderType; + title: string; + subTitle: string; +} & ( + | RecordTableEmptyStateDisplayButtonComponentProps + | RecordTableEmptyStateDisplayButtonProps +); + +export const RecordTableEmptyStateDisplay = ( + props: RecordTableEmptyStateDisplayProps, +) => { const { objectMetadataItem } = useRecordTableContextOrThrow(); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); return ( - + - {title} + + {props.title} + - {subTitle} + {props.subTitle} - {!isReadOnly && ( + {'buttonComponent' in props && props.buttonComponent} + {'buttonTitle' in props && !isReadOnly && (