diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx index 0f7deb15ac..83b4697f30 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx @@ -10,6 +10,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; @@ -47,6 +48,9 @@ export const DeleteMultipleRecordsAction = () => { contextStoreAnyFieldFilterValueComponentState, ); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); + const { filterValueDependencies } = useFilterValueDependencies(); const graphqlFilter = computeContextStoreFilters({ @@ -85,6 +89,7 @@ export const DeleteMultipleRecordsAction = () => { }; const handleDeleteClick = async () => { + removeSelectedRecordsFromRecordBoard(); resetTableRowSelection(); await incrementalDeleteManyRecords(); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx index ba69537ebb..e2de61d2df 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DestroyMultipleRecordsAction.tsx @@ -10,6 +10,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; @@ -32,6 +33,8 @@ export const DestroyMultipleRecordsAction = () => { } const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); const contextStoreTargetedRecordsRule = useRecoilComponentValue( contextStoreTargetedRecordsRuleComponentState, @@ -93,6 +96,7 @@ export const DestroyMultipleRecordsAction = () => { }; const handleDestroyClick = async () => { + removeSelectedRecordsFromRecordBoard(); resetTableRowSelection(); await incrementalDestroyManyRecords(); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx index 2e045d1eef..37975f9ea5 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/RestoreMultipleRecordsAction.tsx @@ -8,6 +8,7 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; @@ -28,6 +29,8 @@ export const RestoreMultipleRecordsAction = () => { } const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); const { restoreManyRecords } = useRestoreManyRecords({ objectNameSingular: objectMetadataItem.nameSingular, @@ -75,6 +78,7 @@ export const RestoreMultipleRecordsAction = () => { }); const handleRestoreClick = async () => { + removeSelectedRecordsFromRecordBoard(); const recordsToRestore = await fetchAllRecordIds(); const recordIdsToRestore = recordsToRestore.map((record) => record.id); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx index 99597b27bc..4564f16a3c 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx @@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { isDefined } from 'twenty-shared/utils'; @@ -15,6 +16,9 @@ export const DeleteSingleRecordAction = () => { const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); + const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, }); @@ -23,6 +27,8 @@ export const DeleteSingleRecordAction = () => { const { deleteFavorite } = useDeleteFavorite(); const handleDeleteClick = async () => { + removeSelectedRecordsFromRecordBoard(); + resetTableRowSelection(); const foundFavorite = favorites?.find( diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DestroySingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DestroySingleRecordAction.tsx index 1e89f27750..0f59ffe752 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DestroySingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DestroySingleRecordAction.tsx @@ -1,6 +1,7 @@ import { ActionModal } from '@/action-menu/actions/components/ActionModal'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { t } from '@lingui/core/macro'; @@ -17,11 +18,15 @@ export const DestroySingleRecordAction = () => { const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); + const { destroyOneRecord } = useDestroyOneRecord({ objectNameSingular: objectMetadataItem.nameSingular, }); const handleDeleteClick = async () => { + removeSelectedRecordsFromRecordBoard(); resetTableRowSelection(); await destroyOneRecord(recordId); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction.tsx index d1341c0f3a..2cf123bc5c 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction.tsx @@ -1,6 +1,7 @@ import { ActionModal } from '@/action-menu/actions/components/ActionModal'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; +import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { t } from '@lingui/core/macro'; @@ -12,12 +13,15 @@ export const RestoreSingleRecordAction = () => { const recordId = useSelectedRecordIdOrThrow(); const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId); + const { removeSelectedRecordsFromRecordBoard } = + useRemoveSelectedRecordsFromRecordBoard(recordIndexId); const { restoreManyRecords } = useRestoreManyRecords({ objectNameSingular: objectMetadataItem.nameSingular, }); const handleRestoreClick = async () => { + removeSelectedRecordsFromRecordBoard(); resetTableRowSelection(); await restoreManyRecords({ diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 3bcfe8c126..369b9c2990 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -24,7 +24,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; -import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; import { useResetFocusStackToRecordIndex } from '@/object-record/record-index/hooks/useResetFocusStackToRecordIndex'; import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow'; @@ -82,7 +82,8 @@ export const PageChangeEffect = () => { const { unfocusRecordTableRow } = useFocusedRecordTableRow(recordIndexId); const { deactivateRecordTableRow } = useActiveRecordTableRow(recordIndexId); - const { resetRecordSelection } = useRecordBoardSelection(recordIndexId); + const { resetRecordBoardSelection } = + useResetRecordBoardSelection(recordIndexId); const { deactivateBoardCard } = useActiveRecordBoardCard(recordIndexId); const { unfocusBoardCard } = useFocusedRecordBoardCard(recordIndexId); @@ -141,7 +142,7 @@ export const PageChangeEffect = () => { deactivateRecordTableRow(); } if (contextStoreCurrentViewType === ContextStoreViewType.Kanban) { - resetRecordSelection(); + resetRecordBoardSelection(); deactivateBoardCard(); unfocusBoardCard(); } @@ -304,7 +305,7 @@ export const PageChangeEffect = () => { resetTableRowSelection, unfocusRecordTableRow, deactivateRecordTableRow, - resetRecordSelection, + resetRecordBoardSelection, deactivateBoardCard, unfocusBoardCard, resetFocusStackToRecordIndex, diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts index 742e90701f..00f3bf870b 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlConnection.ts @@ -5,9 +5,9 @@ export type RecordGqlConnection = { __typename?: string; edges: RecordGqlEdge[]; pageInfo: { - __typename?: string; - hasNextPage?: boolean; - hasPreviousPage?: boolean; + __typename?: Nullable; + hasNextPage?: Nullable; + hasPreviousPage?: Nullable; startCursor?: Nullable; endCursor?: Nullable; totalCount?: number; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx index c99bc51c7d..a429b42aff 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx @@ -1,5 +1,6 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; +import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName'; import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; import { renderHook } from '@testing-library/react'; @@ -22,6 +23,9 @@ describe('useRefetchAggregateQueries', () => { // Arrange const objectMetadataNamePlural = 'opportunities'; const expectedQueryName = getAggregateQueryName(objectMetadataNamePlural); + const expectedQueryNameGroupBy = getGroupByAggregateQueryName({ + objectMetadataNamePlural, + }); // Act const { result } = renderHook(() => @@ -32,7 +36,7 @@ describe('useRefetchAggregateQueries', () => { // Assert expect(mockRefetchQueries).toHaveBeenCalledTimes(1); expect(mockRefetchQueries).toHaveBeenCalledWith({ - include: [expectedQueryName], + include: [expectedQueryName, expectedQueryNameGroupBy], }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts index ff41297296..8b667e3da3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts @@ -1,4 +1,5 @@ import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName'; import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; export const useRefetchAggregateQueries = ({ @@ -11,8 +12,12 @@ export const useRefetchAggregateQueries = ({ const refetchAggregateQueries = async () => { const queryName = getAggregateQueryName(objectMetadataNamePlural); + const groupByAggregateQueryName = getGroupByAggregateQueryName({ + objectMetadataNamePlural, + }); + await apolloCoreClient.refetchQueries({ - include: [queryName], + include: [queryName, groupByAggregateQueryName], }); }; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx index e3eae83640..bb10ee9965 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownCustomView.tsx @@ -2,8 +2,8 @@ import { ObjectOptionsDropdownMenuViewName } from '@/object-record/object-option import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; @@ -55,7 +55,7 @@ export const ObjectOptionsDropdownCustomView = ({ : null; const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, ); const calendarFieldMetadata = currentView?.calendarFieldMetadataId diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx index c5aab81541..fe9af1a410 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx @@ -5,8 +5,8 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; 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 { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -32,7 +32,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { } = useObjectOptionsDropdown(); const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, ); const hiddenRecordGroupIds = useRecoilComponentValue( diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx index bd42b0fd5e..275a37b397 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx @@ -1,8 +1,8 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId'; import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { useSetViewTypeFromLayoutOptionsMenu } from '@/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordIndexCalendarLayoutState } from '@/object-record/record-index/states/recordIndexCalendarLayoutState'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; @@ -65,7 +65,7 @@ export const ObjectOptionsDropdownLayoutContent = () => { recordIndexCalendarLayoutState, ); const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, ); const calendarFieldMetadata = currentView?.calendarFieldMetadataId 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 c262b3a8ec..95b67ca757 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 @@ -5,9 +5,9 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; @@ -52,7 +52,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { ); const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, ); const { 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 6c51f6b163..b65f13d01c 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 @@ -4,9 +4,9 @@ import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropd import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -47,7 +47,7 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { const { currentView } = useGetCurrentViewOnly(); const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, ); const visibleRecordGroupIds = useRecoilComponentFamilyValue( diff --git a/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupByAggregateQuery.ts b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupByAggregateQuery.ts index f6c74c30ff..45f2217663 100644 --- a/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupByAggregateQuery.ts +++ b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupByAggregateQuery.ts @@ -1,8 +1,9 @@ import gql from 'graphql-tag'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getGroupByAggregateQueryName } from '@/object-record/record-aggregate/utils/getGroupByAggregateQueryName'; +import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; import { capitalize } from 'twenty-shared/utils'; -import { getGroupByQueryResultGqlFieldName } from '../../../page-layout/utils/getGroupByQueryResultGqlFieldName'; export const generateGroupByAggregateQuery = ({ objectMetadataItem, @@ -12,7 +13,9 @@ export const generateGroupByAggregateQuery = ({ aggregateOperationGqlFields: string[]; }) => { const capitalizedSingular = capitalize(objectMetadataItem.nameSingular); - const queryName = `${capitalize(objectMetadataItem.namePlural)}GroupBy`; + const queryName = getGroupByAggregateQueryName({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); const queryFieldName = getGroupByQueryResultGqlFieldName(objectMetadataItem); return gql` diff --git a/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery.ts b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery.ts new file mode 100644 index 0000000000..efed78be06 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery.ts @@ -0,0 +1,62 @@ +import gql from 'graphql-tag'; + +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; +import { type ObjectPermissions } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; + +export const generateGroupsRecordsGroupByQuery = ({ + objectMetadataItems, + objectMetadataItem, + recordGqlFields, + computeReferences, + objectPermissionsByObjectMetadataId, +}: { + objectMetadataItems: ObjectMetadataItem[]; + objectMetadataItem: ObjectMetadataItem; + recordGqlFields: RecordGqlOperationGqlRecordFields; + computeReferences?: boolean; + objectPermissionsByObjectMetadataId: Record< + string, + ObjectPermissions & { objectMetadataId: string } + >; +}) => { + const capitalizedSingular = capitalize(objectMetadataItem.nameSingular); + const queryName = `${capitalize(objectMetadataItem.namePlural)}GroupByRecords`; + const queryFieldName = getGroupByQueryResultGqlFieldName(objectMetadataItem); + + return gql` + query ${queryName}( + $groupBy: [${capitalizedSingular}GroupByInput!]! + $filter: ${capitalizedSingular}FilterInput + $orderByForRecords: [${capitalizedSingular}OrderByInput!] + $limit: Int + $offsetForRecords: Int + ) { + ${queryFieldName}( + groupBy: $groupBy + filter: $filter + orderByForRecords: $orderByForRecords + limit: $limit + offsetForRecords: $offsetForRecords + ) { + groupByDimensionValues + edges { + node ${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems, + objectMetadataItem, + recordGqlFields, + computeReferences, + objectPermissionsByObjectMetadataId, + })} + } + pageInfo { + endCursor + hasNextPage + } + } + } + `; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-aggregate/utils/getGroupByAggregateQueryName.ts b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/getGroupByAggregateQueryName.ts new file mode 100644 index 0000000000..63540472f0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-aggregate/utils/getGroupByAggregateQueryName.ts @@ -0,0 +1,9 @@ +import { capitalize } from 'twenty-shared/utils'; + +export const getGroupByAggregateQueryName = ({ + objectMetadataNamePlural, +}: { + objectMetadataNamePlural: string; +}) => { + return `${capitalize(objectMetadataNamePlural)}GroupByAggregates`; +}; 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 8eb191d3d9..084af0b214 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 @@ -1,35 +1,24 @@ import styled from '@emotion/styled'; -// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 + import { useContext, useRef } from 'react'; -import { RecordBoardClickOutsideEffect } from '@/object-record/record-board/components/RecordBoardClickOutsideEffect'; import { RecordBoardColumns } from '@/object-record/record-board/components/RecordBoardColumns'; import { RecordBoardDragDropContext } from '@/object-record/record-board/components/RecordBoardDragDropContext'; import { RecordBoardDragSelect } from '@/object-record/record-board/components/RecordBoardDragSelect'; +import { RecordBoardEffects } from '@/object-record/record-board/components/RecordBoardEffects'; +import { RecordBoardFetchMoreInViewTriggerComponent } from '@/object-record/record-board/components/RecordBoardFetchMoreInViewTriggerComponent'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; -import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect'; -import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect'; -import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; const StyledContainer = styled.div` display: flex; flex: 1; - flex-direction: row; + flex-direction: column; min-height: 100%; position: relative; `; -const StyledColumnContainer = styled.div` - display: flex; - - & > *:not(:first-of-type) { - border-left: 1px solid ${({ theme }) => theme.border.color.light}; - } -`; - const StyledContainerContainer = styled.div` display: flex; flex-direction: column; @@ -48,30 +37,24 @@ export const RecordBoard = () => { const boardRef = useRef(null); return ( - + <> - - - + - - - + + - - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx index ff2adc4caa..287c58c0f3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect.tsx @@ -3,7 +3,7 @@ import { Key } from 'ts-key-enum'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; -import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { useResetFocusStackToRecordIndex } from '@/object-record/record-index/hooks/useResetFocusStackToRecordIndex'; import { PageFocusId } from '@/types/PageFocusId'; @@ -13,7 +13,8 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho export const RecordBoardBodyEscapeHotkeyEffect = () => { const { recordBoardId } = useContext(RecordBoardContext); - const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); + const { resetRecordBoardSelection } = + useResetRecordBoardSelection(recordBoardId); const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); const { resetFocusStackToRecordIndex } = useResetFocusStackToRecordIndex(); @@ -26,9 +27,11 @@ export const RecordBoardBodyEscapeHotkeyEffect = () => { const handleEscape = () => { unfocusBoardCard(); + if (isAtLeastOneRecordSelected) { - resetRecordSelection(); + resetRecordBoardSelection(); } + resetFocusStackToRecordIndex(); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx index c6c9c3162d..2b8d77ee13 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx @@ -4,7 +4,7 @@ import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-b import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; -import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId'; import { isDraggingRecordComponentState } from '@/object-record/record-drag/states/isDraggingRecordComponentState'; @@ -25,7 +25,8 @@ export const RecordBoardClickOutsideEffect = () => { const { deactivateBoardCard } = useActiveRecordBoardCard(recordBoardId); const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); - const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); + const { resetRecordBoardSelection } = + useResetRecordBoardSelection(recordBoardId); useListenClickOutside({ excludedClickOutsideIds: [ @@ -40,7 +41,7 @@ export const RecordBoardClickOutsideEffect = () => { refs: [], callback: () => { if (!isDraggingRecord) { - resetRecordSelection(); + resetRecordBoardSelection(); deactivateBoardCard(); unfocusBoardCard(); } diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardColumns.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardColumns.tsx index fdd9ce7a55..89b0f7ee31 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardColumns.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardColumns.tsx @@ -2,6 +2,15 @@ import { RecordBoardColumn } from '@/object-record/record-board/record-board-col import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { ViewType } from '@/views/types/ViewType'; +import styled from '@emotion/styled'; + +const StyledColumnContainer = styled.div` + display: flex; + + & > *:not(:first-of-type) { + border-left: 1px solid ${({ theme }) => theme.border.color.light}; + } +`; export const RecordBoardColumns = () => { const visibleRecordGroupIds = useRecoilComponentFamilyValue( @@ -9,13 +18,17 @@ export const RecordBoardColumns = () => { ViewType.Kanban, ); - return visibleRecordGroupIds.map((recordGroupId, index) => { - return ( - - ); - }); + return ( + + {visibleRecordGroupIds.map((recordGroupId, index) => { + return ( + + ); + })} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardContainer.tsx similarity index 81% rename from packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx rename to packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardContainer.tsx index 76ac90f68b..10afdca139 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardContainer.tsx @@ -7,23 +7,26 @@ import { RecordBoard } from '@/object-record/record-board/components/RecordBoard import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect'; import { RecordBoardHotkeyEffect } from '@/object-record/record-board/components/RecordBoardHotkeyEffect'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; + import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal'; import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId'; -import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState'; + import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isDefined } from 'twenty-shared/utils'; -type RecordIndexBoardContainerProps = { +type RecordBoardContainerProps = { recordBoardId: string; viewBarId: string; objectNameSingular: string; }; -export const RecordIndexBoardContainer = ({ +export const RecordBoardContainer = ({ recordBoardId, objectNameSingular, -}: RecordIndexBoardContainerProps) => { +}: RecordBoardContainerProps) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -64,12 +67,16 @@ export const RecordIndexBoardContainer = ({ objectPermissions, }} > - - {isRecordIndexRemoveSortingModalOpened && ( - - )} - - + + + {isRecordIndexRemoveSortingModalOpened && ( + + )} + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx index 9a0cb7ff74..e38da043f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardDragDropContext.tsx @@ -38,7 +38,6 @@ export const RecordBoardDragDropContext = ({ const { startRecordDrag } = useStartRecordDrag(); const { endRecordDrag } = useEndRecordDrag(); - // const multiDragState = useRecordDragState('board', recordBoardId); const { processBoardCardDrop } = useProcessBoardCardDrop(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardEffects.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardEffects.tsx new file mode 100644 index 0000000000..87c82fa808 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardEffects.tsx @@ -0,0 +1,19 @@ +import { RecordBoardClickOutsideEffect } from '@/object-record/record-board/components/RecordBoardClickOutsideEffect'; +import { RecordBoardQueryEffect } from '@/object-record/record-board/components/RecordBoardQueryEffect'; +import { RecordBoardScrollToFocusedCardEffect } from '@/object-record/record-board/components/RecordBoardScrollToFocusedCardEffect'; +import { RecordBoardSelectRecordsEffect } from '@/object-record/record-board/components/RecordBoardSelectRecordsEffect'; +import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; +import { RecordBoardDeactivateBoardCardEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardDeactivateBoardCardEffect'; + +export const RecordBoardEffects = () => { + return ( + <> + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardFetchMoreInViewTriggerComponent.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardFetchMoreInViewTriggerComponent.tsx new file mode 100644 index 0000000000..1554ec00be --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardFetchMoreInViewTriggerComponent.tsx @@ -0,0 +1,76 @@ +import styled from '@emotion/styled'; +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; + +import { RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth'; + +import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth'; +import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState'; +import { recordBoardShouldFetchMoreComponentState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreComponentState'; +import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState'; +import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; +import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { ViewType } from '@/views/types/ViewType'; + +const StyledFetchMoreTriggerDiv = styled.div<{ width: number }>` + max-width: ${({ width }) => width}px; + min-width: ${({ width }) => width}px; +`; + +export const RecordBoardFetchMoreInViewTriggerComponent = () => { + const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentState( + recordBoardShouldFetchMoreComponentState, + ); + + const isInitialLoading = useRecoilComponentValue( + recordIndexRecordGroupsAreInInitialLoadingComponentState, + ); + + const isFetchingMore = useRecoilComponentValue( + recordBoardIsFetchingMoreComponentState, + ); + + const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); + + const { ref, inView } = useInView({ + rootMargin: '1600px', + root: scrollWrapperHTMLElement, + }); + + const visibleRecordGroupIds = useRecoilComponentFamilyValue( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, + ); + + const componentWidth = + visibleRecordGroupIds.length * RECORD_BOARD_COLUMN_WIDTH + + visibleRecordGroupIds.length * + RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH - + 1; + + useEffect(() => { + if (!isInitialLoading && !isFetchingMore) { + const newShouldFetchMore = inView; + + if (shouldFetchMore !== newShouldFetchMore) { + setShouldFetchMore(newShouldFetchMore); + } + } + }, [ + shouldFetchMore, + setShouldFetchMore, + inView, + isInitialLoading, + isFetchingMore, + ]); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx new file mode 100644 index 0000000000..f162185b2a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardQueryEffect.tsx @@ -0,0 +1,88 @@ +import { useTriggerRecordBoardFetchMore } from '@/object-record/record-board/hooks/useTriggerRecordBoardFetchMore'; +import { useTriggerRecordBoardInitialQuery } from '@/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery'; +import { lastRecordBoardQueryIdentifierComponentState } from '@/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState'; +import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState'; +import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState'; +import { recordBoardShouldFetchMoreComponentState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreComponentState'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables'; + +import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState'; +import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; +import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +import { useEffect } from 'react'; + +export const RecordBoardQueryEffect = () => { + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const [lastRecordBoardQueryIdentifier, setLastRecordBoardQueryIdentifier] = + useRecoilComponentState(lastRecordBoardQueryIdentifierComponentState); + + const [recordIndexRecordGroupsAreInInitialLoading] = useRecoilComponentState( + recordIndexRecordGroupsAreInInitialLoadingComponentState, + ); + + const setRecordBoardCurrentGroupByQueryOffset = useSetRecoilComponentState( + recordBoardCurrentGroupByQueryOffsetComponentState, + ); + + const { combinedFilters, orderBy } = + useRecordIndexGroupCommonQueryVariables(); + + const queryIdentifier = getQueryIdentifier({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: combinedFilters, + orderBy, + }); + + const queryIdentifierHasChanged = + queryIdentifier !== lastRecordBoardQueryIdentifier; + + const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); + + const [shouldFetchMore] = useRecoilComponentState( + recordBoardShouldFetchMoreComponentState, + ); + + const recordBoardIsFetchingMore = useRecoilComponentValue( + recordBoardIsFetchingMoreComponentState, + ); + + const { triggerRecordBoardFetchMore } = useTriggerRecordBoardFetchMore(); + + const { triggerRecordBoardInitialQuery } = + useTriggerRecordBoardInitialQuery(); + + useEffect(() => { + if ( + !recordIndexRecordGroupsAreInInitialLoading && + queryIdentifierHasChanged + ) { + triggerRecordBoardInitialQuery(); + } else if ( + !recordIndexRecordGroupsAreInInitialLoading && + shouldFetchMore && + !queryIdentifierHasChanged && + !recordBoardIsFetchingMore + ) { + triggerRecordBoardFetchMore(); + } + }, [ + triggerRecordBoardInitialQuery, + queryIdentifierHasChanged, + setLastRecordBoardQueryIdentifier, + queryIdentifier, + setRecordBoardCurrentGroupByQueryOffset, + scrollWrapperHTMLElement, + recordIndexRecordGroupsAreInInitialLoading, + shouldFetchMore, + recordBoardIsFetchingMore, + triggerRecordBoardFetchMore, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardSelectRecordsEffect.tsx similarity index 84% rename from packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx rename to packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardSelectRecordsEffect.tsx index 2b908df7cc..f511d4564b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardSelectRecordsEffect.tsx @@ -5,16 +5,9 @@ import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/r import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; -type RecordIndexBoardDataLoaderEffectProps = { - recordBoardId: string; -}; - -export const RecordIndexBoardDataLoaderEffect = ({ - recordBoardId, -}: RecordIndexBoardDataLoaderEffectProps) => { +export const RecordBoardSelectRecordsEffect = () => { const selectedRecordIds = useRecoilComponentValue( recordBoardSelectedRecordIdsComponentSelector, - recordBoardId, ); const setContextStoreTargetedRecords = useSetRecoilComponentState( diff --git a/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth.ts b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth.ts new file mode 100644 index 0000000000..937633478b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnPaddingAndBorderWidth.ts @@ -0,0 +1 @@ +export const RECORD_BOARD_COLUMN_PADDING_AND_BORDER_WIDTH = 17; diff --git a/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnWidth.ts b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnWidth.ts new file mode 100644 index 0000000000..3e2a2fcec9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardColumnWidth.ts @@ -0,0 +1 @@ +export const RECORD_BOARD_COLUMN_WIDTH = 200; diff --git a/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFetchMoreThrottlingWaitTimeInMillisecondsToAvoidReactFreeze.ts b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFetchMoreThrottlingWaitTimeInMillisecondsToAvoidReactFreeze.ts new file mode 100644 index 0000000000..96b948576a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardFetchMoreThrottlingWaitTimeInMillisecondsToAvoidReactFreeze.ts @@ -0,0 +1 @@ +export const RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE = 50; diff --git a/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardQueryPageSize.ts b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardQueryPageSize.ts new file mode 100644 index 0000000000..f57b6424d5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/constants/RecordBoardQueryPageSize.ts @@ -0,0 +1 @@ +export const RECORD_BOARD_QUERY_PAGE_SIZE = 10; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts index c0422967d9..e0abe4dc31 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardCardHotkeys.ts @@ -4,6 +4,7 @@ import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/use import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; import { useRecordBoardSelectAllHotkeys } from '@/object-record/record-board/hooks/useRecordBoardSelectAllHotkeys'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; @@ -21,8 +22,9 @@ export const useRecordBoardCardHotkeys = (focusId: string) => { const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); const { activateBoardCard } = useActiveRecordBoardCard(); - const { setRecordAsSelected, resetRecordSelection } = - useRecordBoardSelection(); + const { setRecordAsSelected } = useRecordBoardSelection(); + + const { resetRecordBoardSelection } = useResetRecordBoardSelection(); const { unfocusBoardCard } = useFocusedRecordBoardCard(recordBoardId); const isRecordBoardCardSelected = useRecoilComponentFamilyValue( @@ -56,8 +58,9 @@ export const useRecordBoardCardHotkeys = (focusId: string) => { const handleEscape = () => { unfocusBoardCard(); + if (isAtLeastOneRecordSelected) { - resetRecordSelection(); + resetRecordBoardSelection(); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index e807c68618..0cd8adb552 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -32,28 +32,6 @@ export const useRecordBoardSelection = (recordBoardId?: string) => { getActionMenuIdFromRecordIndexId(instanceIdFromProps), ); - const resetRecordSelection = useRecoilCallback( - ({ snapshot, set }) => - () => { - closeDropdown(dropdownId); - - const recordIds = getSnapshotValue( - snapshot, - recordBoardSelectedRecordIdsSelector, - ); - - for (const recordId of recordIds) { - set(isRecordBoardCardSelectedFamilyState(recordId), false); - } - }, - [ - closeDropdown, - dropdownId, - recordBoardSelectedRecordIdsSelector, - isRecordBoardCardSelectedFamilyState, - ], - ); - const setRecordAsSelected = useRecoilCallback( ({ snapshot, set }) => (recordId: string, isSelected: boolean) => { @@ -85,7 +63,6 @@ export const useRecordBoardSelection = (recordBoardId?: string) => { ); return { - resetRecordSelection, setRecordAsSelected, checkIfLastUnselectAndCloseDropdown, }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard.ts new file mode 100644 index 0000000000..f676172116 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard.ts @@ -0,0 +1,101 @@ +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; +import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; + +export const useRemoveSelectedRecordsFromRecordBoard = ( + recordBoardIndexId: string, +) => { + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + recordBoardIndexId, + ); + + const groupByFieldMetadataItem = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + recordBoardIndexId, + ); + + const recordIndexRecordIdsByGroupCallbackState = + useRecoilComponentCallbackState( + recordIndexRecordIdsByGroupComponentFamilyState, + recordBoardIndexId, + ); + + const recordBoardSelectedRecordIdsCallbackSelector = + useRecoilComponentCallbackState( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardIndexId, + ); + + const { resetRecordBoardSelection } = + useResetRecordBoardSelection(recordBoardIndexId); + + const removeSelectedRecordsFromRecordBoard = useRecoilCallback( + ({ snapshot, set }) => + () => { + const deletedRecordIds = getSnapshotValue( + snapshot, + recordBoardSelectedRecordIdsCallbackSelector, + ); + + if ( + !isDefined(groupByFieldMetadataItem) || + !isNonEmptyArray(recordGroupDefinitions) || + !isNonEmptyArray(deletedRecordIds) + ) { + return; + } + + for (const recordGroup of recordGroupDefinitions) { + const currentRecordIds = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackState(recordGroup.id), + ); + + let groupRecordIdsUpdated = [...currentRecordIds]; + + for (const deletedRecordId of deletedRecordIds) { + const indexOfDeletedRecordIdInGroupRecordIds = + groupRecordIdsUpdated.findIndex( + (recordIdInRecordGroup) => + recordIdInRecordGroup === deletedRecordId, + ); + + if (indexOfDeletedRecordIdInGroupRecordIds > -1) { + groupRecordIdsUpdated = groupRecordIdsUpdated.toSpliced( + indexOfDeletedRecordIdInGroupRecordIds, + 1, + ); + } + } + + if (groupRecordIdsUpdated.length !== currentRecordIds.length) { + set( + recordIndexRecordIdsByGroupCallbackState(recordGroup.id), + groupRecordIdsUpdated, + ); + } + } + + resetRecordBoardSelection(); + }, + [ + groupByFieldMetadataItem, + recordIndexRecordIdsByGroupCallbackState, + recordGroupDefinitions, + recordBoardSelectedRecordIdsCallbackSelector, + resetRecordBoardSelection, + ], + ); + + return { + removeSelectedRecordsFromRecordBoard, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetRecordBoardSelection.ts new file mode 100644 index 0000000000..e33a7f9b57 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetRecordBoardSelection.ts @@ -0,0 +1,60 @@ +import { useRecoilCallback } from 'recoil'; + +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; +import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; + +export const useResetRecordBoardSelection = (recordBoardId?: string) => { + const instanceIdFromProps = useAvailableComponentInstanceIdOrThrow( + RecordBoardComponentInstanceContext, + recordBoardId, + ); + + const isRecordBoardCardSelectedFamilyState = useRecoilComponentCallbackState( + isRecordBoardCardSelectedComponentFamilyState, + recordBoardId, + ); + + const recordBoardSelectedRecordIdsSelector = useRecoilComponentCallbackState( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, + ); + + const { closeDropdown } = useCloseDropdown(); + + const dropdownId = getActionMenuDropdownIdFromActionMenuId( + getActionMenuIdFromRecordIndexId(instanceIdFromProps), + ); + + const resetRecordBoardSelection = useRecoilCallback( + ({ snapshot, set }) => + () => { + closeDropdown(dropdownId); + + const recordIds = getSnapshotValue( + snapshot, + recordBoardSelectedRecordIdsSelector, + ); + + for (const recordId of recordIds) { + set(isRecordBoardCardSelectedFamilyState(recordId), false); + } + }, + [ + closeDropdown, + dropdownId, + recordBoardSelectedRecordIdsSelector, + isRecordBoardCardSelectedFamilyState, + ], + ); + + return { + resetRecordBoardSelection, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSelectAllCards.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSelectAllCards.ts index fde616066b..42ec66541f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSelectAllCards.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSelectAllCards.ts @@ -1,6 +1,6 @@ import { useRecoilCallback } from 'recoil'; -import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; +import { useResetRecordBoardSelection } from '@/object-record/record-board/hooks/useResetRecordBoardSelection'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; import { allCardsSelectedStatusComponentSelector } from '@/object-record/record-board/states/selectors/allCardsSelectedStatusComponentSelector'; import { allRecordIdsOfAllRecordGroupsComponentSelector } from '@/object-record/record-index/states/selectors/allRecordIdsOfAllRecordGroupsComponentSelector'; @@ -22,7 +22,8 @@ export const useSelectAllCards = (recordBoardId?: string) => { recordBoardId, ); - const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); + const { resetRecordBoardSelection } = + useResetRecordBoardSelection(recordBoardId); const selectAllCards = useRecoilCallback( ({ set, snapshot }) => @@ -38,7 +39,7 @@ export const useSelectAllCards = (recordBoardId?: string) => { ); if (allCardsSelectedStatus === 'all') { - resetRecordSelection(); + resetRecordBoardSelection(); } for (const recordId of allRecordIds) { @@ -52,7 +53,7 @@ export const useSelectAllCards = (recordBoardId?: string) => { [ allCardsSelectedStatusSelector, allRecordIdsOfAllRecordGroupsCallbackSelector, - resetRecordSelection, + resetRecordBoardSelection, isRecordBoardCardSelectedFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts index e0154977ca..428783d127 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts @@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil'; import { emptyRecordGroupByIdComponentFamilyState } from '@/object-record/record-group/states/emptyRecordGroupByIdComponentFamilyState'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; @@ -12,7 +12,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordIdsForColumn = (recordBoardId?: string) => { const recordGroupFieldMetadataState = useRecoilComponentCallbackState( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, recordBoardId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardFetchMore.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardFetchMore.ts new file mode 100644 index 0000000000..fbd8bbcc51 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardFetchMore.ts @@ -0,0 +1,231 @@ +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE } from '@/object-record/record-board/constants/RecordBoardFetchMoreThrottlingWaitTimeInMillisecondsToAvoidReactFreeze'; +import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize'; + +import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState'; +import { recordBoardIsFetchingMoreComponentState } from '@/object-record/record-board/states/recordBoardIsFetchingMoreComponentState'; +import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables'; +import { useRecordIndexGroupsRecordsLazyGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { sortByProperty } from '~/utils/array/sortByProperty'; +import { sleep } from '~/utils/sleep'; + +export const useTriggerRecordBoardFetchMore = () => { + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + ); + + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const recordIndexGroupFieldMetadataItem = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + + const recordBoardShouldFetchMoreInColumnFamilyCallbackState = + useRecoilComponentCallbackState( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + ); + + const { combinedFilters } = useRecordIndexGroupCommonQueryVariables(); + + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const recordBoardCurrentGroupByQueryOffsetCallbackState = + useRecoilComponentCallbackState( + recordBoardCurrentGroupByQueryOffsetComponentState, + ); + + const { executeRecordIndexGroupsRecordsLazyGroupBy } = + useRecordIndexGroupsRecordsLazyGroupBy({ + groupByFieldMetadataItem: recordIndexGroupFieldMetadataItem, + objectMetadataItem, + }); + + const recordIndexRecordIdsByGroupCallbackState = + useRecoilComponentCallbackState( + recordIndexRecordIdsByGroupComponentFamilyState, + ); + + const recordBoardIsFetchingMoreCallbackState = + useRecoilComponentCallbackState(recordBoardIsFetchingMoreComponentState); + + const triggerRecordBoardFetchMore = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const isAlreadyFetchingMore = getSnapshotValue( + snapshot, + recordBoardIsFetchingMoreCallbackState, + ); + + const cleanStateBeforeExit = () => { + set(recordBoardIsFetchingMoreCallbackState, false); + }; + + if (isAlreadyFetchingMore) { + return; + } + + set(recordBoardIsFetchingMoreCallbackState, true); + + const currentOffset = getSnapshotValue( + snapshot, + recordBoardCurrentGroupByQueryOffsetCallbackState, + ); + + const newOffset = currentOffset + RECORD_BOARD_QUERY_PAGE_SIZE; + + const recordGroupValuesThatShouldBeFetched = recordGroupDefinitions + .filter((recordGroupDefinition) => { + return getSnapshotValue( + snapshot, + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + ); + }) + .map((recordGroupDefinition) => recordGroupDefinition.value) + .filter(isDefined); + + if (!isNonEmptyArray(recordGroupValuesThatShouldBeFetched)) { + cleanStateBeforeExit(); + + return; + } + + const recordIndexGroupsRecordsGroupByLazyQueryResult = + await executeRecordIndexGroupsRecordsLazyGroupBy({ + variables: { + offsetForRecords: newOffset, + filter: { + ...combinedFilters, + [recordIndexGroupFieldMetadataItem?.name ?? '']: { + in: [...recordGroupValuesThatShouldBeFetched], + }, + }, + }, + }); + + set(recordBoardCurrentGroupByQueryOffsetCallbackState, newOffset); + + if (!isDefined(recordIndexGroupsRecordsGroupByLazyQueryResult)) { + cleanStateBeforeExit(); + + return; + } + + const queryFieldName = + getGroupByQueryResultGqlFieldName(objectMetadataItem); + + const groups = + recordIndexGroupsRecordsGroupByLazyQueryResult.data?.[queryFieldName]; + + if (!isDefined(groups)) { + cleanStateBeforeExit(); + + return; + } + + const sortedRecordGroupDefinitions = recordGroupDefinitions.toSorted( + sortByProperty('position'), + ); + + for (const recordGroupDefinition of sortedRecordGroupDefinitions) { + const foundGroupInResult = groups.find( + (recordGroup: any) => + (recordGroup.groupByDimensionValues[0] as string) === + recordGroupDefinition.value, + ); + + if (!isDefined(foundGroupInResult)) { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + continue; + } + + const newRecords = getRecordsFromRecordConnection({ + recordConnection: foundGroupInResult, + }); + + if (!isNonEmptyArray(newRecords)) { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + continue; + } + + const currentRecordIds = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackState(recordGroupDefinition.id), + ); + + const newRecordIds = currentRecordIds.concat( + newRecords.map((record) => record.id), + ); + + set( + recordIndexRecordIdsByGroupCallbackState(recordGroupDefinition.id), + newRecordIds, + ); + + upsertRecordsInStore(newRecords); + + if (newRecords.length < RECORD_BOARD_QUERY_PAGE_SIZE) { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + } else { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + true, + ); + } + + await sleep( + RECORD_BOARD_FETCH_MORE_THROTTLING_WAIT_TIME_IN_MILLISECONDS_TO_AVOID_REACT_FREEZE, + ); + } + + cleanStateBeforeExit(); + }, + [ + objectMetadataItem, + recordGroupDefinitions, + upsertRecordsInStore, + executeRecordIndexGroupsRecordsLazyGroupBy, + recordIndexRecordIdsByGroupCallbackState, + recordBoardIsFetchingMoreCallbackState, + recordBoardCurrentGroupByQueryOffsetCallbackState, + recordBoardShouldFetchMoreInColumnFamilyCallbackState, + combinedFilters, + recordIndexGroupFieldMetadataItem, + ], + ); + + return { + triggerRecordBoardFetchMore, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery.ts new file mode 100644 index 0000000000..6a58df0e54 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useTriggerRecordBoardInitialQuery.ts @@ -0,0 +1,183 @@ +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize'; +import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; +import { lastRecordBoardQueryIdentifierComponentState } from '@/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState'; +import { recordBoardCurrentGroupByQueryOffsetComponentState } from '@/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState'; +import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables'; +import { useRecordIndexGroupsRecordsLazyGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier'; +import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName'; +import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useTriggerRecordBoardInitialQuery = () => { + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + ); + + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const recordIndexGroupFieldMetadataItem = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + + const setLastRecordBoardQueryIdentifier = useSetRecoilComponentState( + lastRecordBoardQueryIdentifierComponentState, + ); + + const recordBoardShouldFetchMoreInColumnFamilyCallbackState = + useRecoilComponentCallbackState( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + ); + + const recordIndexRecordGroupsAreInInitialLoadingCallbackState = + useRecoilComponentCallbackState( + recordIndexRecordGroupsAreInInitialLoadingComponentState, + ); + + const setRecordBoardCurrentGroupByQueryOffset = useSetRecoilComponentState( + recordBoardCurrentGroupByQueryOffsetComponentState, + ); + + const { combinedFilters, orderBy } = + useRecordIndexGroupCommonQueryVariables(); + + const queryIdentifier = getQueryIdentifier({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: combinedFilters, + orderBy, + }); + + const { setRecordIdsForColumn } = useSetRecordIdsForColumn(); + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement(); + + const { executeRecordIndexGroupsRecordsLazyGroupBy } = + useRecordIndexGroupsRecordsLazyGroupBy({ + groupByFieldMetadataItem: recordIndexGroupFieldMetadataItem, + objectMetadataItem, + }); + + const triggerRecordBoardInitialQuery = useRecoilCallback( + ({ set }) => + async () => { + set(recordIndexRecordGroupsAreInInitialLoadingCallbackState, true); + + const cleanStateBeforeExit = () => { + set(recordIndexRecordGroupsAreInInitialLoadingCallbackState, false); + + setLastRecordBoardQueryIdentifier(queryIdentifier); + + setRecordBoardCurrentGroupByQueryOffset(0); + + scrollWrapperHTMLElement?.scrollTo({ top: 0, left: 0 }); + }; + + const recordIndexGroupsRecordsGroupByLazyQueryResult = + await executeRecordIndexGroupsRecordsLazyGroupBy(); + + if (!isDefined(recordIndexGroupsRecordsGroupByLazyQueryResult)) { + cleanStateBeforeExit(); + + return; + } + + const queryFieldName = + getGroupByQueryResultGqlFieldName(objectMetadataItem); + + const groups = + recordIndexGroupsRecordsGroupByLazyQueryResult.data?.[queryFieldName]; + + if (!isDefined(groups)) { + cleanStateBeforeExit(); + + return; + } + + for (const recordGroupDefinition of recordGroupDefinitions) { + const foundGroupInResult = groups?.find( + (recordGroup: any) => + (recordGroup.groupByDimensionValues[0] as string) === + recordGroupDefinition.value, + ); + + if (!isDefined(foundGroupInResult)) { + setRecordIdsForColumn(recordGroupDefinition.id, []); + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + continue; + } + + const records = getRecordsFromRecordConnection({ + recordConnection: foundGroupInResult, + }); + + if (!isNonEmptyArray(records)) { + setRecordIdsForColumn(recordGroupDefinition.id, []); + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + continue; + } + + upsertRecordsInStore(records); + + setRecordIdsForColumn(recordGroupDefinition.id, records); + + if (records.length < RECORD_BOARD_QUERY_PAGE_SIZE) { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + false, + ); + } else { + set( + recordBoardShouldFetchMoreInColumnFamilyCallbackState( + recordGroupDefinition.id, + ), + true, + ); + } + } + + cleanStateBeforeExit(); + }, + [ + recordIndexRecordGroupsAreInInitialLoadingCallbackState, + executeRecordIndexGroupsRecordsLazyGroupBy, + objectMetadataItem, + setLastRecordBoardQueryIdentifier, + queryIdentifier, + setRecordBoardCurrentGroupByQueryOffset, + scrollWrapperHTMLElement, + recordGroupDefinitions, + upsertRecordsInStore, + setRecordIdsForColumn, + recordBoardShouldFetchMoreInColumnFamilyCallbackState, + ], + ); + + return { + triggerRecordBoardInitialQuery, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx index cdb3204dae..94008c863f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SkeletonTheme } from 'react-loading-skeleton'; import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { RecordCardBodyContainer } from '@/object-record/record-card/components/RecordCardBodyContainer'; @@ -8,17 +8,30 @@ import { RecordCardHeaderContainer } from '@/object-record/record-card/component import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; + const StyledSkeletonIconAndText = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(1)}; `; const StyledSkeletonTitle = styled.div` - padding-left: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(1)}; `; -const StyledSeparator = styled.div` - height: ${({ theme }) => theme.spacing(2)}; +const StyledBodyContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(0.5)}; + padding-top: 4px; + padding-bottom: 4px; +`; + +const StyledStaticCellSkeleton = styled.div<{ width: number; height: number }>` + background-color: ${({ theme }) => theme.background.tertiary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + + width: ${({ width }) => width}px; + height: ${({ height }) => height}px; `; export const RecordBoardColumnCardContainerSkeletonLoader = () => { @@ -32,7 +45,7 @@ export const RecordBoardColumnCardContainerSkeletonLoader = () => { visibleRecordFieldsComponentSelector, ); - const numberOfFields = visibleRecordFields.length; + const numberOfFields = visibleRecordFields.length - 1; const skeletonItems = Array.from({ length: numberOfFields }).map( (_, index) => ({ @@ -46,32 +59,30 @@ export const RecordBoardColumnCardContainerSkeletonLoader = () => { - + - - {!isCompactModeActive && - skeletonItems.map(({ id }) => ( - - - - - - - ))} + + {!isCompactModeActive && + skeletonItems.map(({ id }) => ( + + + + + + + ))} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 2129d3c969..2d986eae4e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -1,17 +1,18 @@ import styled from '@emotion/styled'; import { Draggable, type DroppableProvided } from '@hello-pangea/dnd'; import { useContext } from 'react'; -import { useRecoilValue } from 'recoil'; import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer'; -import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader'; -import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader'; + import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewRecordButton'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading'; + +import { RecordBoardColumnLoadingSkeletonCards } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards'; +import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; +import { recordIndexRecordGroupsAreInInitialLoadingComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; -import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; const StyledColumnCardsContainer = styled.div` display: flex; @@ -22,17 +23,6 @@ const StyledColumnCardsContainer = styled.div` const StyledNewButtonContainer = styled.div` padding-bottom: ${({ theme }) => theme.spacing(4)}; `; -// eslint-disable-next-line @nx/workspace-no-hardcoded-colors -const StyledSkeletonCardContainer = styled.div` - background-color: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.background.quaternary}; - border-radius: ${({ theme }) => theme.border.radius.md}; - box-shadow: - 0px 4px 8px 0px rgba(0, 0, 0, 0.08), - 0px 0px 4px 0px rgba(0, 0, 0, 0.08); - color: ${({ theme }) => theme.font.color.primary}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; -`; type RecordBoardColumnCardsContainerProps = { recordBoardColumnId: string; @@ -45,15 +35,18 @@ export const RecordBoardColumnCardsContainer = ({ }: RecordBoardColumnCardsContainerProps) => { const { columnDefinition } = useContext(RecordBoardColumnContext); - const columnId = columnDefinition.id; - const recordIds = useRecoilComponentFamilyValue( recordIndexRecordIdsByGroupComponentFamilyState, recordBoardColumnId, ); - const isRecordIndexBoardColumnLoading = useRecoilValue( - isRecordIndexBoardColumnLoadingFamilyState(columnId), + const recordIndexRecordGroupsAreInInitialLoading = useRecoilComponentValue( + recordIndexRecordGroupsAreInInitialLoadingComponentState, + ); + + const recordBoardShouldFetchMoreInColumn = useRecoilComponentFamilyValue( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + recordBoardColumnId, ); return ( @@ -62,29 +55,20 @@ export const RecordBoardColumnCardsContainer = ({ // eslint-disable-next-line react/jsx-props-no-spreading {...droppableProvided?.droppableProps} > - {isRecordIndexBoardColumnLoading - ? Array.from( - { - length: getNumberOfCardsPerColumnForSkeletonLoading( - columnDefinition.position, - ), - }, - (_, index) => ( - - - - ), - ) - : recordIds.map((recordId, index) => ( - - ))} - + {recordIndexRecordGroupsAreInInitialLoading ? ( + + ) : ( + recordIds.map((recordId, index) => ( + + )) + )} + {recordBoardShouldFetchMoreInColumn ? ( + + ) : null} theme.grayScale.gray9}; - display: flex; - height: 32px; - margin-left: ${({ theme }) => theme.spacing(8)}; - padding-left: ${({ theme }) => theme.spacing(2)}; -`; - -export const RecordBoardColumnFetchMoreLoader = () => { - const { columnDefinition } = useContext(RecordBoardColumnContext); - - const isFetchingRecord = useRecoilValue( - isRecordBoardFetchingRecordsByColumnFamilyState(columnDefinition.id), - ); - - const setShouldFetchMore = useSetRecoilComponentFamilyState( - recordBoardShouldFetchMoreInColumnComponentFamilyState, - columnDefinition.id, - ); - - const { ref, inView } = useInView(); - - useEffect(() => { - setShouldFetchMore(inView); - }, [setShouldFetchMore, inView]); - - return ( -
- {isFetchingRecord && Loading more...} -
- ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 4a3c7ab0fd..d6b10a589c 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -5,16 +5,25 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPe import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; + +import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/record-index/states/recordIndexAggregateDisplayLabelComponentState'; import { recordIndexAggregateDisplayValueForGroupValueComponentFamilyState } from '@/object-record/record-index/states/recordIndexAggregateDisplayValueForGroupValueComponentFamilyState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useRecoilCallback } from 'recoil'; +import { findByProperty, isDefined } from 'twenty-shared/utils'; import { Tag } from 'twenty-ui/components'; import { IconDotsVertical, IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; @@ -56,8 +65,8 @@ const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; display: flex; flex-direction: column; - max-width: 200px; - min-width: 200px; + max-width: ${RECORD_BOARD_COLUMN_WIDTH}px; + min-width: ${RECORD_BOARD_COLUMN_WIDTH}px; padding: ${({ theme }) => theme.spacing(2)}; @@ -95,15 +104,63 @@ export const RecordBoardColumnHeader = () => { recordIndexAggregateDisplayValueForGroupValueComponentFamilyState, { groupValue: columnDefinition?.value ?? '' }, ); + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const recordIndexRecordIdsByGroupCallbackState = + useRecoilComponentCallbackState( + recordIndexRecordIdsByGroupComponentFamilyState, + ); const recordIndexAggregateDisplayLabel = useRecoilComponentValue( recordIndexAggregateDisplayLabelComponentState, ); + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + ); + const { toggleDropdown } = useToggleDropdown(); const dropdownId = `record-board-column-dropdown-${columnDefinition.id}`; + const handleCreateNewRecordClick = useRecoilCallback( + ({ set, snapshot }) => + async () => { + const createdRecord = await createNewIndexRecord({ + position: 'first', + [selectFieldMetadataItem.name]: columnDefinition.value, + }); + + const recordGroup = recordGroupDefinitions.find( + findByProperty('value', createdRecord[selectFieldMetadataItem.name]), + ); + + if (isDefined(recordGroup)) { + const currentRecordIds = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackState(recordGroup.id), + ); + + const newRecordIds = [createdRecord.id, ...currentRecordIds]; + + set( + recordIndexRecordIdsByGroupCallbackState(recordGroup.id), + newRecordIds, + ); + } + + upsertRecordsInStore([createdRecord]); + }, + [ + createNewIndexRecord, + columnDefinition, + recordGroupDefinitions, + recordIndexRecordIdsByGroupCallbackState, + upsertRecordsInStore, + selectFieldMetadataItem, + ], + ); + return ( { { - createNewIndexRecord({ - position: 'first', - [selectFieldMetadataItem.name]: - columnDefinition.value, - }); - }} + onClick={handleCreateNewRecordClick} /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards.tsx new file mode 100644 index 0000000000..0787d407a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnLoadingSkeletonCards.tsx @@ -0,0 +1,27 @@ +import { RECORD_BOARD_QUERY_PAGE_SIZE } from '@/object-record/record-board/constants/RecordBoardQueryPageSize'; +import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader'; +import styled from '@emotion/styled'; + +const StyledSkeletonCardContainer = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.background.quaternary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-shadow: + 0px 4px 8px 0px ${({ theme }) => theme.color.gray2}, + 0px 0px 4px 0px ${({ theme }) => theme.color.gray2}; + color: ${({ theme }) => theme.font.color.primary}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +export const RecordBoardColumnLoadingSkeletonCards = () => { + return Array.from( + { + length: RECORD_BOARD_QUERY_PAGE_SIZE, + }, + (_, index) => ( + + + + ), + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading.ts deleted file mode 100644 index 1f6fc58aaa..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const getNumberOfCardsPerColumnForSkeletonLoading = ( - columnIndex: number, -): number => { - const skeletonCounts: Record = { - 0: 2, - 1: 1, - 2: 3, - 3: 0, - 4: 1, - }; - - return skeletonCounts[columnIndex] || 0; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState.ts new file mode 100644 index 0000000000..26c522751c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/lastRecordBoardQueryIdentifierComponentState.ts @@ -0,0 +1,9 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const lastRecordBoardQueryIdentifierComponentState = + createComponentState({ + key: 'lastRecordBoardQueryIdentifierComponentState', + componentInstanceContext: RecordBoardComponentInstanceContext, + defaultValue: '', + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState.ts new file mode 100644 index 0000000000..b2c631f7ef --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardCurrentGroupByQueryOffsetComponentState.ts @@ -0,0 +1,9 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordBoardCurrentGroupByQueryOffsetComponentState = + createComponentState({ + key: 'recordBoardCurrentGroupByQueryOffsetComponentState', + defaultValue: 0, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardIsFetchingMoreComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardIsFetchingMoreComponentState.ts new file mode 100644 index 0000000000..415f080c25 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardIsFetchingMoreComponentState.ts @@ -0,0 +1,9 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordBoardIsFetchingMoreComponentState = + createComponentState({ + key: 'recordBoardIsFetchingMoreComponentState', + defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreComponentState.ts new file mode 100644 index 0000000000..df2dbf3c55 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreComponentState.ts @@ -0,0 +1,9 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordBoardShouldFetchMoreComponentState = + createComponentState({ + key: 'recordBoardShouldFetchMoreComponentState', + defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessBoardCardDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessBoardCardDrop.ts index 3808934ce3..96a1fdb632 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessBoardCardDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessBoardCardDrop.ts @@ -1,22 +1,24 @@ import { type DropResult } from '@hello-pangea/dnd'; -import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; -import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; - import { processGroupDrop } from '@/object-record/record-drag/utils/processGroupDrop'; + +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { useUpdateDroppedRecordOnBoard } from '@/object-record/record-drag/hooks/useUpdateDroppedRecordOnBoard'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useContext } from 'react'; export const useProcessBoardCardDrop = () => { - const { updateOneRecord, selectFieldMetadataItem } = - useContext(RecordBoardContext); + const { selectFieldMetadataItem } = useContext(RecordBoardContext); - const recordIndexRecordIdsByGroupFamilyState = + const recordIndexRecordIdsByGroupCallbackFamilyState = useRecoilComponentCallbackState( recordIndexRecordIdsByGroupComponentFamilyState, ); + const { updateDroppedRecordOnBoard } = useUpdateDroppedRecordOnBoard(); + const processBoardCardDrop = useRecoilCallback( ({ snapshot }) => (boardCardDropResult: DropResult, selectedRecordIds: string[]) => { @@ -26,22 +28,20 @@ export const useProcessBoardCardDrop = () => { groupDropResult: boardCardDropResult, snapshot, selectedRecordIds, - recordIdsByGroupFamilyState: recordIndexRecordIdsByGroupFamilyState, + recordIdsByGroupFamilyState: + recordIndexRecordIdsByGroupCallbackFamilyState, onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => { - updateOneRecord({ - idToUpdate: recordId, - updateOneRecordInput: { - [selectFieldMetadataItem.name]: targetRecordGroupValue, - position, - }, - }); + updateDroppedRecordOnBoard( + { recordId, position }, + targetRecordGroupValue, + ); }, }); }, [ - updateOneRecord, selectFieldMetadataItem, - recordIndexRecordIdsByGroupFamilyState, + recordIndexRecordIdsByGroupCallbackFamilyState, + updateDroppedRecordOnBoard, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessCalendarCardDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessCalendarCardDrop.ts index 25977872ae..ef92f08460 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessCalendarCardDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessCalendarCardDrop.ts @@ -91,6 +91,7 @@ export const useProcessCalendarCardDrop = () => { arrayOfRecordsWithPosition: recordsWithPosition, idOfItemToMove: recordId, idOfTargetItem: targetRecord.id, + isDroppedAfterList, }); } diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts index d94dc4ec50..dd0a184f57 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useProcessTableWithoutGroupRecordDrop.ts @@ -72,6 +72,10 @@ export const useProcessTableWithoutGroupRecordDrop = () => { selectedRowIdsSelector, ); + const isDroppedAfterList = + tableRecordDropResult.destination.index + 1 >= + allSparseRecordIds.length; + const recordsWithPosition: RecordWithPosition[] = allSparseRecordIds .filter(isDefined) .map((recordId) => ({ @@ -104,6 +108,7 @@ export const useProcessTableWithoutGroupRecordDrop = () => { sourceRecordId: draggedRecordId, targetRecordId: targetRecordId ?? '', recordsWithPosition: contiguousRecordsWithPosition, + isDroppedAfterList, }); if (!isDefined(singleDragResult.position)) { @@ -137,6 +142,7 @@ export const useProcessTableWithoutGroupRecordDrop = () => { targetRecordId: targetRecordId ?? '', selectedRecordIds: originalDragSelection, recordsWithPosition: contiguousRecordsWithPosition, + isDroppedAfterList, }); for (const update of multiDragResult.recordUpdates) { diff --git a/packages/twenty-front/src/modules/object-record/record-drag/hooks/useUpdateDroppedRecordOnBoard.ts b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useUpdateDroppedRecordOnBoard.ts new file mode 100644 index 0000000000..c8a2f3002f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-drag/hooks/useUpdateDroppedRecordOnBoard.ts @@ -0,0 +1,170 @@ +import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; +import { extractRecordPositions } from '@/object-record/record-drag/utils/extractRecordPositions'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; +import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { useContext } from 'react'; +import { useRecoilCallback } from 'recoil'; +import { findByProperty, isDefined } from 'twenty-shared/utils'; +import { sortByProperty } from '~/utils/array/sortByProperty'; + +export const useUpdateDroppedRecordOnBoard = () => { + const { updateOneRecord, selectFieldMetadataItem } = + useContext(RecordBoardContext); + + const recordIndexRecordIdsByGroupCallbackFamilyState = + useRecoilComponentCallbackState( + recordIndexRecordIdsByGroupComponentFamilyState, + ); + + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + ); + + const { upsertRecordsInStore } = useUpsertRecordsInStore(); + + const updateDroppedRecordOnBoard = useRecoilCallback( + ({ snapshot, set }) => + ( + { + recordId, + position: newPosition, + }: { recordId: string; position?: number }, + targetRecordGroupValue: RecordGroupDefinition['value'], + ) => { + const initialRecord = getSnapshotValue( + snapshot, + recordStoreFamilyState(recordId), + ); + + if (!isDefined(newPosition)) { + return; + } + + if (!isDefined(initialRecord)) { + return; + } + + const initialRecordGroupValue = + initialRecord[selectFieldMetadataItem.name]; + + const initialRecordGroup = recordGroupDefinitions.find( + findByProperty('value', initialRecordGroupValue), + ); + + if (!isDefined(initialRecordGroup)) { + return; + } + + const targetRecordGroup = recordGroupDefinitions.find( + findByProperty('value', targetRecordGroupValue), + ); + + if (!isDefined(targetRecordGroup)) { + return; + } + + const initialRecordGroupId = initialRecordGroup.id; + const targetRecordGroupId = targetRecordGroup.id; + + const movingInsideSameRecordGroup = + initialRecordGroupId === targetRecordGroupId; + + const isSamePosition = initialRecord.position === newPosition; + + if (movingInsideSameRecordGroup && isSamePosition) { + return; + } + + const currentRecordIdsInInitialRecordGroup = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackFamilyState(initialRecordGroupId), + ); + + const positionOfDroppedRecordInInitialRecordIds = + currentRecordIdsInInitialRecordGroup.findIndex( + (id) => id === recordId, + ); + + let currentRecordIdsInTargetRecordGroup = getSnapshotValue( + snapshot, + recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId), + ); + + if (positionOfDroppedRecordInInitialRecordIds === -1) { + throw new Error( + `Cannot find record id in initial record group ids on drop, this should not happen`, + ); + } + + const newInitialGroupRecordIds = + currentRecordIdsInInitialRecordGroup.toSpliced( + positionOfDroppedRecordInInitialRecordIds, + 1, + ); + + if (movingInsideSameRecordGroup) { + currentRecordIdsInTargetRecordGroup = newInitialGroupRecordIds; + } else { + set( + recordIndexRecordIdsByGroupCallbackFamilyState( + initialRecordGroupId, + ), + newInitialGroupRecordIds, + ); + } + + const targetGroupRecordsWithIds = extractRecordPositions( + currentRecordIdsInTargetRecordGroup, + snapshot, + ); + + const newTargetRecordGroupWithIds = [ + ...targetGroupRecordsWithIds, + { + id: recordId, + position: newPosition, + }, + ]; + + newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc')); + + set( + recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId), + newTargetRecordGroupWithIds.map((record) => record.id), + ); + + upsertRecordsInStore([ + { + ...initialRecord, + [selectFieldMetadataItem.name]: targetRecordGroupValue, + position: newPosition, + }, + ]); + + updateOneRecord({ + idToUpdate: initialRecord.id, + updateOneRecordInput: { + [selectFieldMetadataItem.name]: targetRecordGroupValue, + position: newPosition, + }, + }); + }, + [ + recordGroupDefinitions, + recordIndexRecordIdsByGroupCallbackFamilyState, + selectFieldMetadataItem, + upsertRecordsInStore, + updateOneRecord, + ], + ); + + return { + updateDroppedRecordOnBoard, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processMultiDrag.test.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processMultiDrag.test.ts index dd9d756edb..5532e0388b 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processMultiDrag.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processMultiDrag.test.ts @@ -17,6 +17,7 @@ describe('processMultiDrag', () => { targetRecordId: 'record-4', selectedRecordIds, recordsWithPosition, + isDroppedAfterList: true, }); expect(dragResult.recordUpdates).toEqual([ @@ -35,6 +36,7 @@ describe('processMultiDrag', () => { targetRecordId: '', selectedRecordIds, recordsWithPosition, + isDroppedAfterList: false, }); }).toThrow('Cannot find item to move for id : '); }); @@ -47,6 +49,7 @@ describe('processMultiDrag', () => { targetRecordId: 'record-3', selectedRecordIds, recordsWithPosition, + isDroppedAfterList: false, }); expect(dragResult.recordUpdates).toEqual([ @@ -62,6 +65,7 @@ describe('processMultiDrag', () => { targetRecordId: 'record-3', selectedRecordIds, recordsWithPosition, + isDroppedAfterList: false, }); expect(dragResult.recordUpdates).toEqual([]); diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts index 689cdb5cc9..4cb8bdea44 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/__tests__/processSingleDrag.test.ts @@ -13,6 +13,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-1', targetRecordId: 'record-3', recordsWithPosition: mockRecordPositionData, + isDroppedAfterList: true, }); expect(dragResult).toEqual({ @@ -26,6 +27,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-3', targetRecordId: 'record-1', recordsWithPosition: mockRecordPositionData, + isDroppedAfterList: false, }); expect(dragResult).toEqual({ @@ -39,6 +41,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-1', targetRecordId: 'record-2', recordsWithPosition: mockRecordPositionData, + isDroppedAfterList: false, }); expect(dragResult).toEqual({ @@ -52,6 +55,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-3', targetRecordId: 'record-2', recordsWithPosition: mockRecordPositionData, + isDroppedAfterList: false, }); expect(dragResult).toEqual({ @@ -65,6 +69,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-4', targetRecordId: 'record-2', recordsWithPosition: mockRecordPositionData, + isDroppedAfterList: false, }); expect(dragResult).toEqual({ @@ -79,6 +84,7 @@ describe('processSingleDrag', () => { sourceRecordId: 'record-1', targetRecordId: 'record-1', recordsWithPosition: [], + isDroppedAfterList: false, }), ).toThrowError('Cannot find item to move for id : record-1'); }); diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/processGroupDrop.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/processGroupDrop.ts index d8cae423f7..4169237287 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/processGroupDrop.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/processGroupDrop.ts @@ -106,6 +106,7 @@ export const processGroupDrop = ({ sourceRecordId: draggedRecordId, targetRecordId: targetRecord.id, recordsWithPosition: recordsWithPosition, + isDroppedAfterList, }); if (!isDefined(singleDragResult.position)) { @@ -125,6 +126,7 @@ export const processGroupDrop = ({ selectedRecordIds, recordsWithPosition, targetRecordId: targetRecord.id, + isDroppedAfterList, }); for (const update of multiDragResult.recordUpdates) { diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/processMultiDrag.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/processMultiDrag.ts index 693aeffe43..77853ed9b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/processMultiDrag.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/processMultiDrag.ts @@ -7,6 +7,7 @@ type MultiDragContext = { targetRecordId: string; selectedRecordIds: string[]; recordsWithPosition: RecordWithPosition[]; + isDroppedAfterList: boolean; }; export const processMultiDrag = ({ @@ -14,12 +15,14 @@ export const processMultiDrag = ({ targetRecordId, selectedRecordIds, recordsWithPosition, + isDroppedAfterList, }: MultiDragContext): MultiDragResult => { const newPositionOfDraggedRecords = computeNewPositionsOfDraggedRecords({ arrayOfRecordsWithPosition: recordsWithPosition, draggedRecordId, targetRecordId, sourceRecordIds: selectedRecordIds, + isDroppedAfterList, }); return { diff --git a/packages/twenty-front/src/modules/object-record/record-drag/utils/processSingleDrag.ts b/packages/twenty-front/src/modules/object-record/record-drag/utils/processSingleDrag.ts index a53077c549..5cac8a3b47 100644 --- a/packages/twenty-front/src/modules/object-record/record-drag/utils/processSingleDrag.ts +++ b/packages/twenty-front/src/modules/object-record/record-drag/utils/processSingleDrag.ts @@ -7,17 +7,20 @@ type SingleDragContext = { targetRecordId: string; sourceRecordId: string; recordsWithPosition: RecordWithPosition[]; + isDroppedAfterList: boolean; }; export const processSingleDrag = ({ targetRecordId, sourceRecordId, recordsWithPosition, + isDroppedAfterList, }: SingleDragContext): RecordWithPosition => { const newPosition = computeNewPositionOfDraggedRecord({ arrayOfRecordsWithPosition: recordsWithPosition, idOfItemToMove: sourceRecordId, idOfTargetItem: targetRecordId, + isDroppedAfterList, }); return { diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts index 9f981f3090..c9bda7a5f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useReorderVisibleRecordFields.ts @@ -35,10 +35,13 @@ export const useReorderVisibleRecordFields = (recordTableId: string) => { const recordToMove = visibleRecordFields[fromIndex]; + const isDroppedAfterList = toIndex >= visibleRecordFields.length; + const newPositionOfTargetRecord = computeNewPositionOfDraggedRecord({ arrayOfRecordsWithPosition: currentRecordFields, idOfItemToMove: idOfRecordToMove, idOfTargetItem: idOfTargetRecord, + isDroppedAfterList, }); updateRecordField(recordToMove.fieldMetadataItemId, { 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 516e58933a..db8f659b79 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 @@ -2,11 +2,11 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { type RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; @@ -49,8 +49,8 @@ export const useRecordGroupActions = ({ objectNameSingular, }); - const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + const recordIndexGroupFieldMetadataItem = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, ); const { handleVisibilityChange: handleRecordGroupVisibilityChange } = @@ -73,13 +73,13 @@ export const useRecordGroupActions = ({ const navigateToSelectSettings = useCallback(() => { setNavigationMemorizedUrl(location.pathname + location.search); - if (!isDefined(recordGroupFieldMetadata)) { + if (!isDefined(recordIndexGroupFieldMetadataItem)) { throw new Error('recordGroupFieldMetadata is not a non-empty string'); } navigate(SettingsPath.ObjectFieldEdit, { objectNamePlural: objectMetadataItem.namePlural, - fieldName: recordGroupFieldMetadata.name, + fieldName: recordIndexGroupFieldMetadataItem.name, }); }, [ setNavigationMemorizedUrl, @@ -87,7 +87,7 @@ export const useRecordGroupActions = ({ location.search, navigate, objectMetadataItem, - recordGroupFieldMetadata, + recordIndexGroupFieldMetadataItem, ]); const hasAccessToDataModelSettings = useHasPermissionFlag( diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts index bebae10d84..5f1db1fab5 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroups.ts @@ -1,17 +1,17 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { type ViewGroup } from '@/views/types/ViewGroup'; import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions'; import { useCallback } from 'react'; import { useRecoilCallback } from 'recoil'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from 'twenty-shared/utils'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordGroups = () => { const setRecordGroups = useRecoilCallback( @@ -48,7 +48,7 @@ export const useSetRecordGroups = () => { : undefined; const currentFieldMetadata = getSnapshotValue( snapshot, - recordGroupFieldMetadataComponentState.atomFamily({ + recordIndexGroupFieldMetadataItemComponentState.atomFamily({ instanceId: recordIndexId, }), ); @@ -56,7 +56,7 @@ export const useSetRecordGroups = () => { // Set the field metadata linked to the record groups if (!isDeeplyEqual(fieldMetadata, currentFieldMetadata)) { set( - recordGroupFieldMetadataComponentState.atomFamily({ + recordIndexGroupFieldMetadataItemComponentState.atomFamily({ instanceId: recordIndexId, }), fieldMetadata, diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts deleted file mode 100644 index 18b1998091..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; - -export const recordGroupFieldMetadataComponentState = createComponentState< - FieldMetadataItem | undefined ->({ - key: 'recordGroupFieldMetadataComponentState', - defaultValue: undefined, - componentInstanceContext: ViewComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx deleted file mode 100644 index 5e615b2064..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState'; -import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn'; -import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; -import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState'; - -export const RecordIndexBoardColumnLoaderEffect = ({ - objectNameSingular, - recordBoardId, - kanbanFieldMetadataItem, - columnId, -}: { - recordBoardId: string; - objectNameSingular: string; - kanbanFieldMetadataItem: FieldMetadataItem; - columnId: string; -}) => { - const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentFamilyState( - recordBoardShouldFetchMoreInColumnComponentFamilyState, - columnId, - recordBoardId, - ); - - const [loadingRecordsForThisColumn, setLoadingRecordsForThisColumn] = - useRecoilState(isRecordBoardFetchingRecordsByColumnFamilyState(columnId)); - - const { fetchMoreRecords, loading, records, hasNextPage } = - useLoadRecordIndexBoardColumn({ - objectNameSingular, - recordBoardId, - kanbanFieldMetadataItem, - columnId, - }); - - const setIsRecordIndexLoading = useSetRecoilState( - isRecordIndexBoardColumnLoadingFamilyState(columnId), - ); - - useEffect(() => { - setIsRecordIndexLoading(loading && records.length === 0); - }, [records, loading, setIsRecordIndexLoading]); - - useEffect(() => { - const run = async () => { - if (!loading && shouldFetchMore && hasNextPage) { - setLoadingRecordsForThisColumn(true); - setShouldFetchMore(false); - - await fetchMoreRecords?.(); - - setLoadingRecordsForThisColumn(false); - } - }; - - run(); - }, [ - setShouldFetchMore, - fetchMoreRecords, - loading, - shouldFetchMore, - setLoadingRecordsForThisColumn, - loadingRecordsForThisColumn, - - hasNextPage, - ]); - - return <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx deleted file mode 100644 index b957b46ca7..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; -import { RecordIndexBoardColumnLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect'; -import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { isDefined } from 'twenty-shared/utils'; - -type RecordIndexBoardDataLoaderProps = { - objectNameSingular: string; - recordBoardId: string; -}; - -export const RecordIndexBoardDataLoader = ({ - objectNameSingular, - recordBoardId, -}: RecordIndexBoardDataLoaderProps) => { - const recordGroupIds = useRecoilComponentValue(recordGroupIdsComponentState); - - const recordIndexGroupFieldMetadataItem = useRecoilComponentValue( - recordIndexGroupFieldMetadataItemComponentState, - ); - - if (!isDefined(recordIndexGroupFieldMetadataItem)) { - return null; - } - - return ( - <> - {recordGroupIds.map((recordGroupId) => ( - - ))} - {recordIndexGroupFieldMetadataItem.isNullable === true && ( - - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 423451be6c..ecebfa9421 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -2,9 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; 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 { RecordBoardContainer } from '@/object-record/record-board/components/RecordBoardContainer'; import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer'; import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; @@ -15,7 +13,6 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; -import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewType } from '@/views/types/ViewType'; @@ -68,21 +65,15 @@ export const RecordIndexContainer = () => { {recordIndexViewType === ViewType.Table && ( <> - )} {recordIndexViewType === ViewType.Kanban && ( - - - )} {recordIndexViewType === ViewType.Calendar && ( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect.tsx index e329876d93..69b46501a4 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect.tsx @@ -1,4 +1,5 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { recordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useAggregateGqlFieldsFromRecordIndexGroupAggregates } from '@/object-record/record-index/hooks/useAggregateGqlFieldsFromRecordIndexGroupAggregates'; import { useRecordIndexGroupsAggregatesGroupBy } from '@/object-record/record-index/hooks/useRecordIndexGroupsAggregatesGroupBy'; @@ -9,6 +10,7 @@ import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/ import { turnRecordIndexGroupByAggregateQueryResultIntoRecordAggregateValueByGroupValue } from '@/object-record/record-index/utils/turnRecordIndexGroupByAggregateQueryResultIntoRecordAggregateValueByGroupValue'; import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useEffect } from 'react'; import { type Nullable } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -49,6 +51,10 @@ export const RecordIndexGroupAggregateQueryEffect = ({ const { setRecordIndexAggregateDisplayValueForRecordGroupValue } = useSetRecordIndexAggregateDisplayValueForRecordGroupValue(); + const recordGroupDefinitions = useRecoilComponentValue( + recordGroupDefinitionsComponentSelector, + ); + useEffect(() => { if ( !loading && @@ -71,13 +77,29 @@ export const RecordIndexGroupAggregateQueryEffect = ({ recordIndexGroupAggregateFieldMetadataItem, ); - for (const recordAggregateValueByGroupValue of recordAggregateValueByGroupValueArray) { - setRecordIndexAggregateDisplayValueForRecordGroupValue( - recordIndexGroupAggregateOperation, - recordIndexGroupAggregateFieldMetadataItem, - recordAggregateValueByGroupValue.recordGroupValue, - recordAggregateValueByGroupValue.recordAggregateValue, - ); + for (const recordGroupDefinition of recordGroupDefinitions) { + const foundAggregateValueForGroup = + recordAggregateValueByGroupValueArray.find( + (recordAggregateValueByGroupValue) => + recordAggregateValueByGroupValue.recordGroupValue === + recordGroupDefinition.value, + ); + + if (isDefined(foundAggregateValueForGroup)) { + setRecordIndexAggregateDisplayValueForRecordGroupValue( + recordIndexGroupAggregateOperation, + recordIndexGroupAggregateFieldMetadataItem, + foundAggregateValueForGroup.recordGroupValue, + foundAggregateValueForGroup.recordAggregateValue, + ); + } else { + setRecordIndexAggregateDisplayValueForRecordGroupValue( + recordIndexGroupAggregateOperation, + recordIndexGroupAggregateFieldMetadataItem, + recordGroupDefinition.value ?? '', + 0, + ); + } } } } @@ -92,6 +114,7 @@ export const RecordIndexGroupAggregateQueryEffect = ({ recordAggregateGqlField, recordIndexAggregateDisplayLabelCallbackState, objectMetadataItem, + recordGroupDefinitions, ]); return null; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader.tsx index 04faaf6430..b1e70e83f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader.tsx @@ -1,7 +1,7 @@ import { RecordIndexGroupAggregateQueryEffect } from '@/object-record/record-index/components/RecordIndexGroupAggregateQueryEffect'; import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState'; import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState'; -import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { isDefined } from 'twenty-shared/utils'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts index 9d51ea2a7a..9282a6cd82 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts @@ -14,8 +14,8 @@ import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdow import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { type RecordField } from '@/object-record/record-field/types/RecordField'; import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { ViewType } from '@/views/types/ViewType'; import { isDefined } from 'twenty-shared/utils'; @@ -59,7 +59,7 @@ export const useRecordIndexLazyFetchRecords = ({ }); const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, + recordIndexGroupFieldMetadataItemComponentState, recordIndexId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts deleted file mode 100644 index 5a20afa5da..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; -import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; -import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; -import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; - -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; - -import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; -import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; -import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { - combineFilters, - computeRecordGqlOperationFilter, - isDefined, - turnAnyFieldFilterIntoRecordGqlFilter, -} from 'twenty-shared/utils'; - -type UseLoadRecordIndexBoardProps = { - objectNameSingular: string; - kanbanFieldMetadataItem: FieldMetadataItem; - recordBoardId: string; - columnId: string; -}; - -export const useLoadRecordIndexBoardColumn = ({ - objectNameSingular, - kanbanFieldMetadataItem, - recordBoardId, - columnId, -}: UseLoadRecordIndexBoardProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId); - const { upsertRecordsInStore } = useUpsertRecordsInStore(); - - const recordGroupDefinition = useRecoilValue( - recordGroupDefinitionFamilyState(columnId), - ); - - const currentRecordFilterGroups = useRecoilComponentValue( - currentRecordFilterGroupsComponentState, - ); - - const currentRecordFilters = useRecoilComponentValue( - currentRecordFiltersComponentState, - ); - - const currentRecordSorts = useRecoilComponentValue( - currentRecordSortsComponentState, - ); - - const { filterValueDependencies } = useFilterValueDependencies(); - - const requestFilters = computeRecordGqlOperationFilter({ - filterValueDependencies, - recordFilters: currentRecordFilters, - recordFilterGroups: currentRecordFilterGroups, - fields: objectMetadataItem.fields, - }); - - const anyFieldFilterValue = useRecoilComponentValue( - anyFieldFilterValueComponentState, - ); - - const { recordGqlOperationFilter: anyFieldFilter } = - turnAnyFieldFilterIntoRecordGqlFilter({ - fields: objectMetadataItem.fields, - filterValue: anyFieldFilterValue, - }); - - const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts); - const recordGroupFieldMetadata = useRecoilComponentValue( - recordGroupFieldMetadataComponentState, - recordBoardId, - ); - - const recordGqlFields = useRecordsFieldVisibleGqlFields({ - objectMetadataItem, - additionalFieldMetadataId: recordGroupFieldMetadata?.id, - }); - - const recordIndexKanbanFieldMetadataFilterValue = isDefined( - recordGroupDefinition?.value, - ) - ? { in: [recordGroupDefinition?.value] } - : { is: 'NULL' }; - - const combinedFilters = combineFilters([ - anyFieldFilter, - requestFilters, - { - [kanbanFieldMetadataItem.name]: recordIndexKanbanFieldMetadataFilterValue, - }, - ]); - - const { records, loading, fetchMoreRecords, queryIdentifier, hasNextPage } = - useFindManyRecords({ - objectNameSingular, - filter: combinedFilters, - orderBy, - recordGqlFields, - limit: 10, - }); - - useEffect(() => { - setRecordIdsForColumn(columnId, records); - }, [records, setRecordIdsForColumn, columnId]); - - useEffect(() => { - upsertRecordsInStore(records); - }, [records, upsertRecordsInStore]); - - return { - records, - loading, - fetchMoreRecords, - queryIdentifier, - hasNextPage, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts index ad6f6bdbd3..c3eda9e8fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexStates.ts @@ -6,12 +6,12 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; import { recordIndexCalendarFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexCalendarFieldMetadataIdState'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState'; import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState'; -import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts new file mode 100644 index 0000000000..b7591c308a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables.ts @@ -0,0 +1,69 @@ +import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { useRecordsFieldVisibleGqlFields } from '@/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields'; +import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; +import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { + combineFilters, + computeRecordGqlOperationFilter, + turnAnyFieldFilterIntoRecordGqlFilter, +} from 'twenty-shared/utils'; + +export const useRecordIndexGroupCommonQueryVariables = () => { + const { objectMetadataItem } = useRecordIndexContextOrThrow(); + + const currentRecordFilterGroups = useRecoilComponentValue( + currentRecordFilterGroupsComponentState, + ); + + const currentRecordFilters = useRecoilComponentValue( + currentRecordFiltersComponentState, + ); + + const currentRecordSorts = useRecoilComponentValue( + currentRecordSortsComponentState, + ); + + const { filterValueDependencies } = useFilterValueDependencies(); + + const requestFilters = computeRecordGqlOperationFilter({ + filterValueDependencies, + recordFilters: currentRecordFilters, + recordFilterGroups: currentRecordFilterGroups, + fields: objectMetadataItem.fields, + }); + + const anyFieldFilterValue = useRecoilComponentValue( + anyFieldFilterValueComponentState, + ); + + const { recordGqlOperationFilter: anyFieldFilter } = + turnAnyFieldFilterIntoRecordGqlFilter({ + fields: objectMetadataItem.fields, + filterValue: anyFieldFilterValue, + }); + + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, currentRecordSorts); + + const recordGroupFieldMetadata = useRecoilComponentValue( + recordIndexGroupFieldMetadataItemComponentState, + ); + + const recordGqlFields = useRecordsFieldVisibleGqlFields({ + objectMetadataItem, + additionalFieldMetadataId: recordGroupFieldMetadata?.id, + }); + + const combinedFilters = combineFilters([anyFieldFilter, requestFilters]); + + return { + combinedFilters, + recordGqlFields, + orderBy, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts new file mode 100644 index 0000000000..998d6554df --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexGroupsRecordsLazyGroupBy.ts @@ -0,0 +1,71 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { type RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { generateGroupsRecordsGroupByQuery } from '@/object-record/record-aggregate/utils/generateGroupsRecordsGroupByQuery'; +import { useRecordIndexGroupCommonQueryVariables } from '@/object-record/record-index/hooks/useRecordIndexGroupCommonQueryVariables'; +import { buildGroupByFieldObject } from '@/page-layout/widgets/graph/utils/buildGroupByFieldObject'; +import { useLazyQuery } from '@apollo/client'; +import { type Nullable } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { type PageInfo } from '~/generated/graphql'; + +export type GroupsRecordsGroupByLazyResult = { + [queryGqlFieldName: string]: Array<{ + groupByDimensionValues: string[]; + edges: RecordGqlEdge[]; + pageInfo: PageInfo; + __typename: string; + }>; +}; + +export const useRecordIndexGroupsRecordsLazyGroupBy = ({ + objectMetadataItem, + groupByFieldMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; + groupByFieldMetadataItem: Nullable; +}) => { + const { objectMetadataItems } = useObjectMetadataItems(); + const apolloCoreClient = useApolloCoreClient(); + + const { combinedFilters, orderBy, recordGqlFields } = + useRecordIndexGroupCommonQueryVariables(); + + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const recordIndexGroupsRecordGroupsGroupByQuery = + generateGroupsRecordsGroupByQuery({ + objectMetadataItem, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + recordGqlFields, + }); + + const groupByGqlInput = isDefined(groupByFieldMetadataItem) + ? buildGroupByFieldObject({ + field: groupByFieldMetadataItem, + }) + : {}; + + const [executeRecordIndexGroupsRecordsLazyGroupBy] = + useLazyQuery( + recordIndexGroupsRecordGroupsGroupByQuery, + { + variables: { + filter: { ...combinedFilters }, + groupBy: { + ...groupByGqlInput, + }, + orderByForRecords: orderBy, + }, + client: apolloCoreClient, + }, + ); + + return { + executeRecordIndexGroupsRecordsLazyGroupBy, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState.ts similarity index 54% rename from packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState.ts index e672db6a61..96e0b8ec00 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataItemComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState.ts @@ -1,10 +1,10 @@ -import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; export const recordIndexGroupFieldMetadataItemComponentState = - createComponentState({ + createComponentState({ key: 'recordIndexGroupFieldMetadataItemComponentState', - defaultValue: null, - componentInstanceContext: ContextStoreComponentInstanceContext, + defaultValue: undefined, + componentInstanceContext: ViewComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState.ts new file mode 100644 index 0000000000..43c73df127 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupsAreInInitialLoadingComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordIndexRecordGroupsAreInInitialLoadingComponentState = + createComponentState({ + key: 'recordIndexRecordGroupsAreInInitialLoadingComponentState', + defaultValue: false, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 4b54c98181..aba93ef692 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -1,4 +1,5 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect'; import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView'; import { RecordTable } from '@/object-record/record-table/components/RecordTable'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; @@ -67,6 +68,7 @@ export const RecordTableWithWrappers = ({ > + diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/__snapshots__/generateGroupByQuery.test.ts.snap b/packages/twenty-front/src/modules/object-record/utils/__tests__/__snapshots__/generateGroupByQuery.test.ts.snap index 2e4c896a78..b31a2f9c93 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/__snapshots__/generateGroupByQuery.test.ts.snap +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/__snapshots__/generateGroupByQuery.test.ts.snap @@ -2,7 +2,7 @@ exports[`generateGroupByQuery should generate valid GraphQL query for empty aggregate operations 1`] = ` " - query PeopleGroupBy( + query PeopleGroupByAggregates( $groupBy: [PersonGroupByInput!]! $filter: PersonFilterInput $orderBy: [PersonOrderByWithGroupByInput!] @@ -24,7 +24,7 @@ exports[`generateGroupByQuery should generate valid GraphQL query for empty aggr exports[`generateGroupByQuery should generate valid GraphQL query for multiple aggregate operations 1`] = ` " - query OpportunitiesGroupBy( + query OpportunitiesGroupByAggregates( $groupBy: [OpportunityGroupByInput!]! $filter: OpportunityFilterInput $orderBy: [OpportunityOrderByWithGroupByInput!] @@ -49,7 +49,7 @@ exports[`generateGroupByQuery should generate valid GraphQL query for multiple a exports[`generateGroupByQuery should generate valid GraphQL query for single aggregate operation 1`] = ` " - query OpportunitiesGroupBy( + query OpportunitiesGroupByAggregates( $groupBy: [OpportunityGroupByInput!]! $filter: OpportunityFilterInput $orderBy: [OpportunityOrderByWithGroupByInput!] diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts index 7fbcb30e9f..fb608e8de3 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeNewPositionOfRecordWithPosition.test.ts @@ -23,43 +23,106 @@ const mockRecordsWithPosition: RecordWithPosition[] = [ ]; describe('computeNewPositionOfRecordWithPosition', () => { - it('should compute first position', () => { - const newPosition = computeNewPositionOfDraggedRecord({ - arrayOfRecordsWithPosition: mockRecordsWithPosition, - idOfItemToMove: 'B', - idOfTargetItem: 'A', + describe('dragging inside same list', () => { + it('should compute first position', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'B', + idOfTargetItem: 'A', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(-1); }); - expect(newPosition).toEqual(-1); + it('should compute last position', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'B', + idOfTargetItem: 'D', + isDroppedAfterList: true, + }); + + expect(newPosition).toEqual(4); + }); + + it('should compute intermediary position after target item', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'A', + idOfTargetItem: 'B', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(1.5); + }); + + it('should compute intermediary position before target item', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'A', + idOfTargetItem: 'C', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(2.5); + }); }); - it('should compute last position', () => { - const newPosition = computeNewPositionOfDraggedRecord({ - arrayOfRecordsWithPosition: mockRecordsWithPosition, - idOfItemToMove: 'B', - idOfTargetItem: 'D', + describe('dragging from another list', () => { + it('should compute first position', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'Unknown', + idOfTargetItem: 'A', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(-1); }); - expect(newPosition).toEqual(4); - }); + it('should compute last position', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'Unknown', + idOfTargetItem: 'D', + isDroppedAfterList: true, + }); - it('should compute intermediary position after target item', () => { - const newPosition = computeNewPositionOfDraggedRecord({ - arrayOfRecordsWithPosition: mockRecordsWithPosition, - idOfItemToMove: 'A', - idOfTargetItem: 'B', + expect(newPosition).toEqual(4); }); - expect(newPosition).toEqual(1.5); - }); + it('should compute position before last item', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'Unknown', + idOfTargetItem: 'D', + isDroppedAfterList: false, + }); - it('should compute intermediary position before target item', () => { - const newPosition = computeNewPositionOfDraggedRecord({ - arrayOfRecordsWithPosition: mockRecordsWithPosition, - idOfItemToMove: 'A', - idOfTargetItem: 'C', + expect(newPosition).toEqual(2.5); }); - expect(newPosition).toEqual(2.5); + it('should compute intermediary position after target item', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'Unknown', + idOfTargetItem: 'B', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(0.5); + }); + + it('should compute intermediary position before target item', () => { + const newPosition = computeNewPositionOfDraggedRecord({ + arrayOfRecordsWithPosition: mockRecordsWithPosition, + idOfItemToMove: 'Unknown', + idOfTargetItem: 'C', + isDroppedAfterList: false, + }); + + expect(newPosition).toEqual(1.5); + }); }); }); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfDraggedRecord.ts b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfDraggedRecord.ts index 0a22b4ea8c..716ab612a7 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfDraggedRecord.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionOfDraggedRecord.ts @@ -10,10 +10,12 @@ export const computeNewPositionOfDraggedRecord = ({ arrayOfRecordsWithPosition, idOfItemToMove, idOfTargetItem, + isDroppedAfterList, }: { arrayOfRecordsWithPosition: RecordWithPosition[]; idOfItemToMove: string; idOfTargetItem: string; + isDroppedAfterList: boolean; }) => { const targetItem = arrayOfRecordsWithPosition.find( (recordToFind) => recordToFind.id === idOfTargetItem, @@ -29,6 +31,10 @@ export const computeNewPositionOfDraggedRecord = ({ const targetPosition = targetItem.position; + if (isDroppedAfterList) { + return targetPosition + 1; + } + const sortedRecordsByAscendingPosition = arrayOfRecordsWithPosition.toSorted( sortByProperty('position'), ); @@ -43,16 +49,10 @@ export const computeNewPositionOfDraggedRecord = ({ (recordToFind) => recordToFind.id === idOfTargetItem, ); - const lastIndex = sortedRecordsByAscendingPosition.length - 1; - const shouldGoToFirstPosition = indexOfTargetItem === 0; - const shouldGoToLastPosition = indexOfTargetItem === lastIndex; - if (shouldGoToFirstPosition) { return targetPosition - 1; - } else if (shouldGoToLastPosition) { - return targetPosition + 1; } else { if (itemToMoveIsNotInTable) { const itemBeforeTargetItem = diff --git a/packages/twenty-front/src/modules/object-record/utils/computeNewPositionsOfDraggedRecords.ts b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionsOfDraggedRecords.ts index 32f5502038..8c3fc805a7 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeNewPositionsOfDraggedRecords.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeNewPositionsOfDraggedRecords.ts @@ -8,11 +8,13 @@ export const computeNewPositionsOfDraggedRecords = ({ draggedRecordId, targetRecordId, sourceRecordIds, + isDroppedAfterList, }: { arrayOfRecordsWithPosition: RecordWithPosition[]; draggedRecordId: string; targetRecordId: string; sourceRecordIds: string[]; + isDroppedAfterList: boolean; }): RecordWithPosition[] | null => { const targetItem = arrayOfRecordsWithPosition.find( (recordToFind) => recordToFind.id === targetRecordId, @@ -38,12 +40,8 @@ export const computeNewPositionsOfDraggedRecords = ({ (recordToFind) => recordToFind.id === targetRecordId, ); - const lastIndex = arrayOfRecordsWithPosition.length - 1; - const shouldGoToFirstPosition = indexOfTargetItem === 0; - const shouldGoToLastPosition = indexOfTargetItem === lastIndex; - if (shouldGoToFirstPosition) { const newPositions = computeNewEvenlySpacedPositions({ startingPosition: targetPosition - 1, @@ -58,7 +56,7 @@ export const computeNewPositionsOfDraggedRecords = ({ })); return newSourceRecordsWithPosition; - } else if (shouldGoToLastPosition) { + } else if (isDroppedAfterList) { const newPositions = computeNewEvenlySpacedPositions({ startingPosition: targetPosition, endingPosition: targetPosition + sourceRecordIds.length + 1, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index ffb3ac62f3..21747d7a03 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -158,6 +158,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic queryRunnerContext, orderByForRecords: args.orderByForRecords ?? [], groupLimit: args.limit, + offsetForRecords: args.offsetForRecords, }); } diff --git a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts index 10c26de59e..ddb5c451ef 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-query-args.type.ts @@ -71,6 +71,7 @@ export interface GroupByQueryArgs { includeRecords?: boolean; selectedFields: CommonSelectedFields; limit?: number; + offsetForRecords?: number; } export interface DestroyOneQueryArgs { id: string; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 62d70c022e..27d953065a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -1,3 +1,5 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'class-validator'; import { type OrderByWithGroupBy } from 'twenty-shared/types'; import { type FindOptionsWhere, type ObjectLiteral } from 'typeorm'; @@ -115,6 +117,36 @@ export class GraphqlQueryParser { return queryBuilder.orderBy(parsedOrderBys); } + public getOrderByRawSQL( + orderBy: ObjectRecordOrderBy | OrderByWithGroupBy, + objectNameSingular: string, + isForwardPagination = true, + ): string { + const parsedOrderBys = this.orderFieldParser.parse( + orderBy as ObjectRecordOrderBy, + objectNameSingular, + isForwardPagination, + ); + + const orderByRawSQLClauseArray = Object.entries(parsedOrderBys).map( + ([orderByField, orderByCondition]) => { + const nullsCondition = isDefined(orderByCondition.nulls) + ? ` ${orderByCondition.nulls}` + : ''; + + return `${orderByField} ${orderByCondition.order}${nullsCondition}`; + }, + ); + + const orderByRawSQLString = orderByRawSQLClauseArray.join(', '); + + const orderByCompleteSQLClause = isNonEmptyString(orderByRawSQLString) + ? `ORDER BY ${orderByRawSQLString}` + : ''; + + return orderByCompleteSQLClause; + } + public applyGroupByOrderToBuilder( // eslint-disable-next-line @typescript-eslint/no-explicit-any queryBuilder: WorkspaceSelectQueryBuilder, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts index 3cff2bfa5f..75c0d3059b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; +import { isNonEmptyString } from '@sniptt/guards'; import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -44,6 +45,7 @@ export class GroupByWithRecordsService { queryRunnerContext, orderByForRecords, groupLimit, + offsetForRecords, }: { queryBuilderWithGroupBy: WorkspaceSelectQueryBuilder; queryBuilderWithFiltersAndWithoutGroupBy: WorkspaceSelectQueryBuilder; @@ -52,6 +54,7 @@ export class GroupByWithRecordsService { queryRunnerContext: CommonExtendedQueryRunnerContext; orderByForRecords: ObjectRecordOrderBy; groupLimit?: number; + offsetForRecords?: number; }): Promise { const effectiveGroupLimit = getGroupLimit(groupLimit); @@ -91,16 +94,21 @@ export class GroupByWithRecordsService { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps, + offsetForRecords, }); const recordsResult = await queryBuilderWithPartitionBy.getRawMany(); + const allRecords = recordsResult + .flatMap((group) => group.records) + .filter(isDefined); + if (!isEmpty(selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ flatObjectMetadataMaps, flatFieldMetadataMaps, parentObjectMetadataItem: flatObjectMetadata, - parentObjectRecords: recordsResult.flatMap((group) => group.records), + parentObjectRecords: allRecords, parentObjectRecordsAggregatedValues: {}, relations: selectedFieldsResult.relations, aggregate: selectedFieldsResult.aggregate, @@ -141,6 +149,7 @@ export class GroupByWithRecordsService { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps, + offsetForRecords = 0, }: { queryBuilderForSubQuery: WorkspaceSelectQueryBuilder; columnsToSelect: Record; @@ -151,11 +160,8 @@ export class GroupByWithRecordsService { flatObjectMetadata: FlatObjectMetadata; flatObjectMetadataMaps: FlatEntityMaps; flatFieldMetadataMaps: FlatEntityMaps; + offsetForRecords?: number; }): WorkspaceSelectQueryBuilder { - const groupByExpressions = groupByDefinitions - .map((def) => def.expression) - .join(', '); - const groupByAliases = groupByDefinitions .map((def) => `"${def.alias}"`) .join(', '); @@ -179,9 +185,17 @@ export class GroupByWithRecordsService { const subQuery = queryBuilderForSubQuery .select(recordSelectWithAlias) .addSelect(groupBySelectWithAlias) - .addSelect(`ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions})`, 'rn') .andWhere(groupConditions); + this.applyPartitionByToBuilder({ + groupByDefinitions, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + orderByForRecords, + queryBuilder: subQuery, + }); + if (!isEmpty(orderByForRecords)) { const graphqlQueryParser = new GraphqlQueryParser( flatObjectMetadata, @@ -198,16 +212,17 @@ export class GroupByWithRecordsService { let mainQueryQueryBuilder = repository.createQueryBuilder(); + const pageStart = offsetForRecords; + const pageEnd = offsetForRecords + RECORDS_PER_GROUP_LIMIT; + const mainQuery = mainQueryQueryBuilder .from(`(${subQuery.getQuery()})`, 'ranked_records') .setParameters(queryBuilderForSubQuery.expressionMap.parameters) - .where('rn <= :recordsPerGroupLimit', { - recordsPerGroupLimit: RECORDS_PER_GROUP_LIMIT, - }) + .select(groupByAliases) .addSelect( `JSON_AGG( - CASE WHEN rn <= ${RECORDS_PER_GROUP_LIMIT} THEN + CASE WHEN record_row_number > ${pageStart} AND record_row_number <= ${pageEnd} THEN JSON_BUILD_OBJECT( ${[ ...Object.keys(columnsToSelect).map( @@ -219,7 +234,7 @@ export class GroupByWithRecordsService { ].join(',\n ')} ) END - ) FILTER (WHERE rn <= ${RECORDS_PER_GROUP_LIMIT})`, + ) FILTER (WHERE record_row_number > ${pageStart} AND record_row_number <= ${pageEnd})`, 'records', ) .groupBy(groupByAliases); @@ -232,6 +247,53 @@ export class GroupByWithRecordsService { return mainQuery as WorkspaceSelectQueryBuilder; } + private applyPartitionByToBuilder({ + groupByDefinitions, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + orderByForRecords, + queryBuilder, + }: { + queryBuilder: WorkspaceSelectQueryBuilder; + groupByDefinitions: GroupByDefinition[]; + orderByForRecords: ObjectRecordOrderBy; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + }) { + const groupByExpressions = groupByDefinitions + .map((def) => def.expression) + .join(', '); + + const hasOrderByForRecords = !isEmpty(orderByForRecords); + + if (hasOrderByForRecords) { + const graphqlQueryParser = new GraphqlQueryParser( + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + + const orderByRawSQL = graphqlQueryParser.getOrderByRawSQL( + orderByForRecords, + flatObjectMetadata.nameSingular, + ); + + if (isNonEmptyString(orderByRawSQL)) { + return queryBuilder.addSelect( + `ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions} ${orderByRawSQL})`, + 'record_row_number', + ); + } + } + + return queryBuilder.addSelect( + `ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions})`, + 'record_row_number', + ); + } + private buildGroupConditions( groupsResult: Array>, groupByDefinitions: GroupByDefinition[], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index f7c88999bd..870fb9dfff 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -64,6 +64,7 @@ export interface GroupByResolverArgs { viewId?: string; orderBy?: OrderByWithGroupBy; orderByForRecords?: ObjectRecordOrderBy; + offsetForRecords?: number; limit?: number; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index a8b5018c05..606e72c18c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -190,6 +190,10 @@ export const getResolverArgs = ( type: GraphQLInt, isNullable: true, }, + offsetForRecords: { + type: GraphQLInt, + isNullable: true, + }, }; default: throw new Error(`Unknown resolver type: ${type}`);