From fb0221b4c180ca27bced37cc928f3007a4fee024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Mon, 18 Nov 2024 15:36:40 +0100 Subject: [PATCH] feat: default record group table (#8397) This PR is preparing states to we'll be able to handle view groups correctly with table data. RowIds are now stores in 2 component states, one storing ids by view group and another storing all the rowIds. We're doing that because some other state like focus, or select must not be scoped by view group id. --- .../__tests__/useObjectRecordTable.test.tsx | 18 +++-- .../hooks/useCurrentRecordGroupDefinition.ts | 37 +++++++++ .../hooks/useCurrentRecordGroupId.ts | 20 +++++ .../hooks/useRecordGroupReorder.ts | 2 +- .../states/context/RecordGroupContext.ts | 9 +++ ...RecordGroupDefinitionsComponentSelector.ts | 21 +++++ .../hooks/useLoadRecordIndexTable.ts | 40 ++++++++- .../record-table/components/RecordTable.tsx | 48 +++++++---- ...s.tsx => RecordTableNoRecordGroupRows.tsx} | 8 +- .../components/RecordTableRecordGroupRows.tsx | 26 ++++++ .../components/RecordTableStickyEffect.tsx | 49 +++++++++++ .../components/RecordTableEmptyHandler.tsx | 43 ++++++++++ .../internal/useResetTableRowSelection.ts | 12 +-- .../hooks/internal/useSelectAllRows.ts | 18 +++-- .../hooks/internal/useSetRecordTableData.ts | 74 +++++++++++++---- .../hooks/useRecordTableMoveFocus.ts | 81 +++++++++---------- .../RecordTableBodyDragDropContext.tsx | 8 +- ...y.tsx => RecordTableNoRecordGroupBody.tsx} | 14 ++-- ...=> RecordTableNoRecordGroupBodyEffect.tsx} | 54 ++----------- .../RecordTableRecordGroupBodyEffect.tsx | 79 ++++++++++++++++++ .../RecordTableRecordGroupBodyEffects.tsx | 18 +++++ .../RecordTableRecordGroupsBody.tsx | 47 +++++++++++ .../components/RecordTableHeader.tsx | 6 +- .../components/RecordTableHeaderCell.tsx | 6 +- .../allRowsSelectedStatusComponentSelector.ts | 10 ++- .../selectedRowIdsComponentSelector.ts | 8 +- .../unselectedRowIdsComponentSelector.ts | 8 +- ...ate.ts => tableAllRowIdsComponentState.ts} | 4 +- .../tableRecordGroupIdsComponentState.ts | 11 +++ .../tableRowIdsByGroupComponentFamilyState.ts | 10 +++ .../utils/createComponentFamilySelectorV2.ts | 27 ++++++- .../utils/createComponentFamilyStateV2.ts | 15 +++- 32 files changed, 654 insertions(+), 177 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupDefinition.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupContext.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts rename packages/twenty-front/src/modules/object-record/record-table/components/{RecordTableRows.tsx => RecordTableNoRecordGroupRows.tsx} (68%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx rename packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/{RecordTableBody.tsx => RecordTableNoRecordGroupBody.tsx} (69%) rename packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/{RecordTableBodyEffect.tsx => RecordTableNoRecordGroupBodyEffect.tsx} (67%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx rename packages/twenty-front/src/modules/object-record/record-table/states/{tableRowIdsComponentState.ts => tableAllRowIdsComponentState.ts} (73%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/tableRecordGroupIdsComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx index 58828525ea7..7008bec9249 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx @@ -3,8 +3,10 @@ import { renderHook } from '@testing-library/react'; import { ReactNode } from 'react'; import { mocks } from '@/auth/hooks/__mocks__/useAuth'; +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const recordTableId = 'people'; @@ -23,12 +25,18 @@ const Wrapper = ({ children }: { children: ReactNode }) => { return ( - - {children} - + + + {children} + + + ); diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupDefinition.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupDefinition.ts new file mode 100644 index 00000000000..78afc403371 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupDefinition.ts @@ -0,0 +1,37 @@ +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { hasRecordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useContext, useMemo } from 'react'; + +export const useCurrentRecordGroupDefinition = (recordTableId?: string) => { + const context = useContext(RecordGroupContext); + + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupDefinitionsComponentSelector, + recordTableId, + ); + + const recordGroupDefinitions = useRecoilComponentValueV2( + recordGroupDefinitionsComponentState, + recordTableId, + ); + + const recordGroupDefinition = useMemo(() => { + if (!hasRecordGroups) { + return undefined; + } + + if (!context) { + throw new Error( + 'useCurrentRecordGroupDefinition must be used within a RecordGroupContextProvider.', + ); + } + + return recordGroupDefinitions.find( + ({ id }) => id === context.recordGroupId, + ); + }, [context, hasRecordGroups, recordGroupDefinitions]); + + return recordGroupDefinition; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts new file mode 100644 index 00000000000..b9960a19c6c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts @@ -0,0 +1,20 @@ +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { useContext } from 'react'; + +export const useCurrentRecordGroupId = () => { + const context = useContext(RecordGroupContext); + + if (!context) { + throw new Error( + 'useCurrentRecordGroupId must be used within a RecordGroupContextProvider.', + ); + } + + if (!context.recordGroupId) { + throw new Error( + 'RecordGroupContext is malformed. recordGroupId is missing.', + ); + } + + return context.recordGroupId; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts index 97151a5838c..b0c738a0fbc 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts @@ -23,7 +23,7 @@ export const useRecordGroupReorder = ({ ); const { visibleRecordGroups } = useRecordGroups({ - objectNameSingular, + objectNameSingular: objectNameSingular, }); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupContext.ts b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupContext.ts new file mode 100644 index 00000000000..402d8743de7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react'; + +export type RecordGroupContextProps = { + recordGroupId: string; +}; + +export const RecordGroupContext = createContext( + {} as RecordGroupContextProps, +); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts new file mode 100644 index 00000000000..bccab902cc8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts @@ -0,0 +1,21 @@ +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const hasRecordGroupDefinitionsComponentSelector = + createComponentSelectorV2({ + key: 'hasRecordGroupDefinitionsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupDefinitions = get( + recordGroupDefinitionsComponentState.atomFamily({ + instanceId, + }), + ); + + return recordGroupDefinitions.length > 0; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index a2b0e03480b..22f2043cf4e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -6,6 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState'; @@ -14,6 +15,8 @@ import { tableViewFilterGroupsComponentState } from '@/object-record/record-tabl import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isNull } from '@sniptt/guards'; +import { useMemo } from 'react'; +import { isDefined } from 'twenty-ui'; import { WorkspaceActivationStatus } from '~/generated/graphql'; export const useFindManyParams = ( @@ -24,6 +27,9 @@ export const useFindManyParams = ( objectNameSingular, }); + const currentRecordGroupDefinition = + useCurrentRecordGroupDefinition(recordTableId); + const tableViewFilterGroups = useRecoilComponentValueV2( tableViewFilterGroupsComponentState, recordTableId, @@ -37,15 +43,45 @@ export const useFindManyParams = ( recordTableId, ); - const filter = computeViewRecordGqlOperationFilter( + const stateFilter = computeViewRecordGqlOperationFilter( tableFilters, objectMetadataItem?.fields ?? [], tableViewFilterGroups, ); + const recordGroupFilter = useMemo(() => { + if (isDefined(currentRecordGroupDefinition)) { + const fieldMetadataItem = objectMetadataItem?.fields.find( + (fieldMetadataItem) => + fieldMetadataItem.id === currentRecordGroupDefinition.fieldMetadataId, + ); + + if (!fieldMetadataItem) { + return {}; + } + + return { + [fieldMetadataItem.name]: { + eq: currentRecordGroupDefinition.value, + }, + }; + } + + // TODO: Handle case when value is nullable + + return {}; + }, [objectMetadataItem.fields, currentRecordGroupDefinition]); + const orderBy = turnSortsIntoOrderBy(objectMetadataItem, tableSorts); - return { objectNameSingular, filter, orderBy }; + return { + objectNameSingular, + filter: { + ...stateFilter, + ...recordGroupFilter, + }, + orderBy, + }; }; export const useLoadRecordIndexTable = (objectNameSingular: string) => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 485f4b04987..ac2fd976d25 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -1,18 +1,22 @@ import styled from '@emotion/styled'; import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { hasRecordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector'; import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance'; import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider'; +import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody'; -import { RecordTableBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyEffect'; import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect'; +import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody'; +import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect'; +import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects'; +import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody'; import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -40,17 +44,17 @@ export const RecordTable = ({ }: RecordTableProps) => { const tableBodyRef = useRef(null); + const { toggleClickOutsideListener } = useClickOutsideListener( + RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, + ); + const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, recordTableId, ); - const { toggleClickOutsideListener } = useClickOutsideListener( - RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, - ); - const tableRowIds = useRecoilComponentValueV2( - tableRowIdsComponentState, + tableAllRowIdsComponentState, recordTableId, ); @@ -59,15 +63,20 @@ export const RecordTable = ({ recordTableId, ); - const { resetTableRowSelection, setRowSelected } = useRecordTable({ + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupDefinitionsComponentSelector, recordTableId, - }); + ); const recordTableIsEmpty = !isRecordTableInitialLoading && tableRowIds.length === 0 && isNull(pendingRecordId); + const { resetTableRowSelection, setRowSelected } = useRecordTable({ + recordTableId, + }); + if (!isNonEmptyString(objectNameSingular)) { return <>; } @@ -82,7 +91,11 @@ export const RecordTable = ({ recordTableId={recordTableId} viewBarId={viewBarId} > - + {!hasRecordGroups ? ( + + ) : ( + + )} - - + + {!hasRecordGroups ? ( + + ) : ( + + )} + { - const tableRowIds = useRecoilComponentValueV2(tableRowIdsComponentState); +export const RecordTableNoRecordGroupRows = () => { + const rowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); return ( <> - {tableRowIds.map((recordId, rowIndex) => { + {rowIds.map((recordId, rowIndex) => { return ( { + const recordGroupId = useCurrentRecordGroupId(); + + const allRowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); + + const recordGroupRowIds = useRecoilComponentFamilyValueV2( + tableRowIdsByGroupComponentFamilyState, + recordGroupId, + ); + + return recordGroupRowIds.map((recordId) => { + // Find the index of the recordId in allRowIds + const rowIndex = allRowIds.indexOf(recordId); + + return ( + + ); + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx new file mode 100644 index 00000000000..5ed26708134 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableStickyEffect.tsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; + +import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; +import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue'; +import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; + +export const RecordTableStickyEffect = () => { + const scrollTop = useScrollTopValue('recordTableWithWrappers'); + + useEffect(() => { + if (scrollTop > 0) { + document + .getElementById('record-table-header') + ?.classList.add('header-sticky'); + } else { + document + .getElementById('record-table-header') + ?.classList.remove('header-sticky'); + } + }, [scrollTop]); + + const scrollLeft = useScrollLeftValue('recordTableWithWrappers'); + + const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2( + isRecordTableScrolledLeftComponentState, + ); + + useEffect(() => { + setIsRecordTableScrolledLeft(scrollLeft === 0); + if (scrollLeft > 0) { + document + .getElementById('record-table-body') + ?.classList.add('first-columns-sticky'); + document + .getElementById('record-table-header') + ?.classList.add('first-columns-sticky'); + } else { + document + .getElementById('record-table-body') + ?.classList.remove('first-columns-sticky'); + document + .getElementById('record-table-header') + ?.classList.remove('first-columns-sticky'); + } + }, [scrollLeft, setIsRecordTableScrolledLeft]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx new file mode 100644 index 00000000000..7b28090002e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx @@ -0,0 +1,43 @@ +import { isNull } from '@sniptt/guards'; + +import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; +import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; +import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +type RecordTableEmptyHandlerProps = { + recordTableId: string; + children: React.ReactNode; +}; + +export const RecordTableEmptyHandler = ({ + recordTableId, + children, +}: RecordTableEmptyHandlerProps) => { + const isRecordTableInitialLoading = useRecoilComponentValueV2( + isRecordTableInitialLoadingComponentState, + recordTableId, + ); + + const tableRowIds = useRecoilComponentValueV2( + tableAllRowIdsComponentState, + recordTableId, + ); + + const pendingRecordId = useRecoilComponentValueV2( + recordTablePendingRecordIdComponentState, + recordTableId, + ); + + const recordTableIsEmpty = + !isRecordTableInitialLoading && + tableRowIds.length === 0 && + isNull(pendingRecordId); + + if (recordTableIsEmpty) { + return ; + } + + return children; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index 8780f623abb..82b51a2e164 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -5,7 +5,7 @@ import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionM import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; @@ -18,8 +18,8 @@ export const useResetTableRowSelection = (recordTableId?: string) => { recordTableId, ); - const tableRowIdsState = useRecoilComponentCallbackStateV2( - tableRowIdsComponentState, + const tableAllRowIdsState = useRecoilComponentCallbackStateV2( + tableAllRowIdsComponentState, recordTableIdFromContext, ); @@ -41,9 +41,9 @@ export const useResetTableRowSelection = (recordTableId?: string) => { ); return useRecoilCallback( - ({ snapshot, set }) => + ({ set, snapshot }) => () => { - const tableRowIds = getSnapshotValue(snapshot, tableRowIdsState); + const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); for (const rowId of tableRowIds) { set(isRowSelectedFamilyState(rowId), false); @@ -54,7 +54,7 @@ export const useResetTableRowSelection = (recordTableId?: string) => { set(isActionMenuDropdownOpenState, false); }, [ - tableRowIdsState, + tableAllRowIdsState, hasUserSelectedAllRowsState, isActionMenuDropdownOpenState, isRowSelectedFamilyState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts index 38b8e75c212..24d54fb1ba2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts @@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -11,14 +11,14 @@ export const useSelectAllRows = (recordTableId?: string) => { allRowsSelectedStatusComponentSelector, recordTableId, ); - const tableRowIdsState = useRecoilComponentCallbackStateV2( - tableRowIdsComponentState, - recordTableId, - ); const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2( isRowSelectedComponentFamilyState, recordTableId, ); + const tableAllRowIdsState = useRecoilComponentCallbackStateV2( + tableAllRowIdsComponentState, + recordTableId, + ); const selectAllRows = useRecoilCallback( ({ set, snapshot }) => @@ -28,7 +28,7 @@ export const useSelectAllRows = (recordTableId?: string) => { allRowsSelectedStatusSelector, ); - const tableRowIds = getSnapshotValue(snapshot, tableRowIdsState); + const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); if ( allRowsSelectedStatus === 'none' || @@ -43,7 +43,11 @@ export const useSelectAllRows = (recordTableId?: string) => { } } }, - [allRowsSelectedStatusSelector, tableRowIdsState, isRowSelectedFamilyState], + [ + allRowsSelectedStatusSelector, + tableAllRowIdsState, + isRowSelectedFamilyState, + ], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 4b23ac749bf..7a35ef87a6a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -1,14 +1,16 @@ import { useRecoilCallback } from 'recoil'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; -import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; +import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; type useSetRecordTableDataProps = { recordTableId?: string; @@ -19,12 +21,12 @@ export const useSetRecordTableData = ({ recordTableId, onEntityCountChange, }: useSetRecordTableDataProps) => { - const tableRowIdsState = useRecoilComponentCallbackStateV2( - tableRowIdsComponentState, + const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + tableRowIdsByGroupComponentFamilyState, recordTableId, ); - const numberOfTableRowsState = useRecoilComponentCallbackStateV2( - numberOfTableRowsComponentState, + const tableAllRowIdsState = useRecoilComponentCallbackStateV2( + tableAllRowIdsComponentState, recordTableId, ); const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2( @@ -35,11 +37,23 @@ export const useSetRecordTableData = ({ hasUserSelectedAllRowsComponentState, recordTableId, ); + const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( + recordGroupDefinitionsComponentState, + recordTableId, + ); return useRecoilCallback( ({ set, snapshot }) => - (newRecords: T[], totalCount?: number) => { - for (const record of newRecords) { + ({ + records, + recordGroupId, + totalCount, + }: { + records: T[]; + recordGroupId?: string; + totalCount?: number; + }) => { + for (const record of records) { // TODO: refactor with scoped state later const currentRecord = snapshot .getLoadable(recordStoreFamilyState(record.id)) @@ -50,14 +64,24 @@ export const useSetRecordTableData = ({ } } - const currentRowIds = getSnapshotValue(snapshot, tableRowIdsState); + const currentRowIds = getSnapshotValue( + snapshot, + recordGroupId + ? tableRowIdsByGroupFamilyState(recordGroupId) + : tableAllRowIdsState, + ); const hasUserSelectedAllRows = getSnapshotValue( snapshot, hasUserSelectedAllRowsState, ); - const recordIds = newRecords.map((record) => record.id); + const recordGroupDefinitions = getSnapshotValue( + snapshot, + recordGroupDefinitionsState, + ); + + const recordIds = records.map((record) => record.id); if (!isDeeplyEqual(currentRowIds, recordIds)) { if (hasUserSelectedAllRows) { @@ -66,14 +90,36 @@ export const useSetRecordTableData = ({ } } - set(tableRowIdsState, recordIds); - set(numberOfTableRowsState, totalCount ?? 0); + if (isDefined(recordGroupId)) { + // TODO: Hack to store all ids in the same order as the record group definitions + // Should be replaced by something more efficient + const allRowIds: string[] = []; + + set(tableRowIdsByGroupFamilyState(recordGroupId), recordIds); + + for (const recordGroupDefinition of recordGroupDefinitions) { + const tableRowIdsByGroup = + recordGroupDefinition.id !== recordGroupId + ? getSnapshotValue( + snapshot, + tableRowIdsByGroupFamilyState(recordGroupDefinition.id), + ) + : recordIds; + + allRowIds.push(...tableRowIdsByGroup); + } + set(tableAllRowIdsState, allRowIds); + } else { + set(tableAllRowIdsState, recordIds); + } + onEntityCountChange(totalCount); } }, [ - numberOfTableRowsState, - tableRowIdsState, + tableRowIdsByGroupFamilyState, + tableAllRowIdsState, + recordGroupDefinitionsState, onEntityCountChange, isRowSelectedFamilyState, hasUserSelectedAllRowsState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts index c6ef772521a..7694d5f757a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts @@ -3,9 +3,9 @@ import { useRecoilCallback } from 'recoil'; import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; -import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition'; @@ -17,6 +17,11 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { recordTableId, ); + const tableAllRowIdsState = useRecoilComponentCallbackStateV2( + tableAllRowIdsComponentState, + recordTableId, + ); + const moveUp = useRecoilCallback( ({ snapshot }) => () => { @@ -25,50 +30,41 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { softFocusPositionState, ); - let newRowNumber = softFocusPosition.row - 1; + let newRowIndex = softFocusPosition.row - 1; - if (newRowNumber < 0) { - newRowNumber = 0; + if (newRowIndex < 0) { + newRowIndex = 0; } setSoftFocusPosition({ ...softFocusPosition, - row: newRowNumber, + row: newRowIndex, }); }, [softFocusPositionState, setSoftFocusPosition], ); - const numberOfTableRowsState = useRecoilComponentCallbackStateV2( - numberOfTableRowsComponentState, - recordTableId, - ); - const moveDown = useRecoilCallback( ({ snapshot }) => () => { + const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, ); - const numberOfTableRows = getSnapshotValue( - snapshot, - numberOfTableRowsState, - ); + let newRowIndex = softFocusPosition.row + 1; - let newRowNumber = softFocusPosition.row + 1; - - if (newRowNumber >= numberOfTableRows) { - newRowNumber = numberOfTableRows - 1; + if (newRowIndex >= allRowIds.length) { + newRowIndex = allRowIds.length - 1; } setSoftFocusPosition({ ...softFocusPosition, - row: newRowNumber, + row: newRowIndex, }); }, - [numberOfTableRowsState, setSoftFocusPosition, softFocusPositionState], + [tableAllRowIdsState, setSoftFocusPosition, softFocusPositionState], ); const numberOfTableColumnsSelector = useRecoilComponentCallbackStateV2( @@ -79,6 +75,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { const moveRight = useRecoilCallback( ({ snapshot }) => () => { + const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, @@ -89,24 +86,18 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { numberOfTableColumnsSelector, ); - const numberOfTableRows = getSnapshotValue( - snapshot, - numberOfTableRowsState, - ); - - const currentColumnNumber = softFocusPosition.column; - const currentRowNumber = softFocusPosition.row; + const currentColumnIndex = softFocusPosition.column; + const currentRowIndex = softFocusPosition.row; const isLastRowAndLastColumn = - currentColumnNumber === numberOfTableColumns - 1 && - currentRowNumber === numberOfTableRows - 1; + currentColumnIndex === numberOfTableColumns - 1 && + currentRowIndex === allRowIds.length - 1; const isLastColumnButNotLastRow = - currentColumnNumber === numberOfTableColumns - 1 && - currentRowNumber !== numberOfTableRows - 1; + currentColumnIndex === numberOfTableColumns - 1 && + currentRowIndex !== allRowIds.length - 1; - const isNotLastColumn = - currentColumnNumber !== numberOfTableColumns - 1; + const isNotLastColumn = currentColumnIndex !== numberOfTableColumns - 1; if (isLastRowAndLastColumn) { return; @@ -114,20 +105,20 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { if (isNotLastColumn) { setSoftFocusPosition({ - row: currentRowNumber, - column: currentColumnNumber + 1, + row: currentRowIndex, + column: currentColumnIndex + 1, }); } else if (isLastColumnButNotLastRow) { setSoftFocusPosition({ - row: currentRowNumber + 1, + row: currentRowIndex + 1, column: 0, }); } }, [ + tableAllRowIdsState, softFocusPositionState, numberOfTableColumnsSelector, - numberOfTableRowsState, setSoftFocusPosition, ], ); @@ -145,16 +136,16 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { numberOfTableColumnsSelector, ); - const currentColumnNumber = softFocusPosition.column; - const currentRowNumber = softFocusPosition.row; + const currentColumnIndex = softFocusPosition.column; + const currentRowIndex = softFocusPosition.row; const isFirstRowAndFirstColumn = - currentColumnNumber === 0 && currentRowNumber === 0; + currentColumnIndex === 0 && currentRowIndex === 0; const isFirstColumnButNotFirstRow = - currentColumnNumber === 0 && currentRowNumber > 0; + currentColumnIndex === 0 && currentRowIndex > 0; - const isNotFirstColumn = currentColumnNumber > 0; + const isNotFirstColumn = currentColumnIndex > 0; if (isFirstRowAndFirstColumn) { return; @@ -162,12 +153,12 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { if (isNotFirstColumn) { setSoftFocusPosition({ - row: currentRowNumber, - column: currentColumnNumber - 1, + row: currentRowIndex, + column: currentColumnIndex - 1, }); } else if (isFirstColumnButNotFirstRow) { setSoftFocusPosition({ - row: currentRowNumber - 1, + row: currentRowIndex - 1, column: numberOfTableColumns - 1, }); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx index 64b33fb374d..0bcc4ac78cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx @@ -6,7 +6,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; @@ -22,7 +22,9 @@ export const RecordTableBodyDragDropContext = ({ objectNameSingular, }); - const tableRowIds = useRecoilComponentValueV2(tableRowIdsComponentState); + const tableAllRowIds = useRecoilComponentValueV2( + tableAllRowIdsComponentState, + ); const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(recordTableId); @@ -41,7 +43,7 @@ export const RecordTableBodyDragDropContext = ({ return; } - const computeResult = computeNewRowPosition(result, tableRowIds); + const computeResult = computeNewRowPosition(result, tableAllRowIds); if (!isDefined(computeResult)) { return; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx similarity index 69% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx index 8084985a2d4..f2a765d6d47 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx @@ -1,20 +1,22 @@ -import { RecordTableRows } from '@/object-record/record-table/components/RecordTableRows'; +import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows'; import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -export const RecordTableBody = () => { - const tableRowIds = useRecoilComponentValueV2(tableRowIdsComponentState); +export const RecordTableNoRecordGroupBody = () => { + const tableAllRowIds = useRecoilComponentValueV2( + tableAllRowIdsComponentState, + ); const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, ); - if (isRecordTableInitialLoading && tableRowIds.length === 0) { + if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { return ; } @@ -22,7 +24,7 @@ export const RecordTableBody = () => { - + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx similarity index 67% rename from packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx rename to packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx index f66b6643d33..76a29fccab6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx @@ -7,20 +7,17 @@ import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useL import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; -import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; -import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue'; -import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { isNonEmptyString } from '@sniptt/guards'; import { useScrollToPosition } from '~/hooks/useScrollToPosition'; -export const RecordTableBodyEffect = () => { +export const RecordTableNoRecordGroupBodyEffect = () => { const { objectNameSingular } = useContext(RecordTableContext); - const [hasInitializedScroll, setHasInitiazedScroll] = useState(false); + const [hasInitializedScroll, setHasInitializedScroll] = useState(false); const { fetchMoreRecords, @@ -40,51 +37,11 @@ export const RecordTableBodyEffect = () => { tableLastRowVisibleComponentState, ); - const scrollTop = useScrollTopValue('recordTableWithWrappers'); - const setHasRecordTableFetchedAllRecordsComponents = useSetRecoilComponentStateV2( hasRecordTableFetchedAllRecordsComponentStateV2, ); - // TODO: move this outside because it might cause way too many re-renders for other hooks - useEffect(() => { - if (scrollTop > 0) { - document - .getElementById('record-table-header') - ?.classList.add('header-sticky'); - } else { - document - .getElementById('record-table-header') - ?.classList.remove('header-sticky'); - } - }, [scrollTop]); - - const scrollLeft = useScrollLeftValue('recordTableWithWrappers'); - - const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2( - isRecordTableScrolledLeftComponentState, - ); - - useEffect(() => { - setIsRecordTableScrolledLeft(scrollLeft === 0); - if (scrollLeft > 0) { - document - .getElementById('record-table-body') - ?.classList.add('first-columns-sticky'); - document - .getElementById('record-table-header') - ?.classList.add('first-columns-sticky'); - } else { - document - .getElementById('record-table-body') - ?.classList.remove('first-columns-sticky'); - document - .getElementById('record-table-header') - ?.classList.remove('first-columns-sticky'); - } - }, [scrollLeft, setIsRecordTableScrolledLeft]); - const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState( lastShowPageRecordIdState, ); @@ -106,7 +63,7 @@ export const RecordTableBodyEffect = () => { scrollToPosition(positionInPx); - setHasInitiazedScroll(true); + setHasInitializedScroll(true); } } }, [ @@ -120,7 +77,10 @@ export const RecordTableBodyEffect = () => { useEffect(() => { if (!loading) { - setRecordTableData(records, totalCount); + setRecordTableData({ + records, + totalCount, + }); } }, [records, totalCount, setRecordTableData, loading]); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx new file mode 100644 index 00000000000..831f1dc4fdd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx @@ -0,0 +1,79 @@ +import { useContext, useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; +import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId'; +import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight'; +import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; +import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useScrollToPosition } from '~/hooks/useScrollToPosition'; + +export const RecordTableRecordGroupBodyEffect = () => { + const { objectNameSingular } = useContext(RecordTableContext); + + const recordGroupId = useCurrentRecordGroupId(); + + const [hasInitializedScroll, setHasInitializedScroll] = useState(false); + + const { records, totalCount, setRecordTableData, loading, hasNextPage } = + useLoadRecordIndexTable(objectNameSingular); + + const setHasRecordTableFetchedAllRecordsComponents = + useSetRecoilComponentStateV2( + hasRecordTableFetchedAllRecordsComponentStateV2, + ); + + const [lastShowPageRecordId, setLastShowPageRecordId] = useRecoilState( + lastShowPageRecordIdState, + ); + + const { scrollToPosition } = useScrollToPosition(); + + useEffect(() => { + if (isNonEmptyString(lastShowPageRecordId) && !hasInitializedScroll) { + const isRecordAlreadyFetched = records.some( + (record) => record.id === lastShowPageRecordId, + ); + + if (isRecordAlreadyFetched) { + const recordPosition = records.findIndex( + (record) => record.id === lastShowPageRecordId, + ); + + const positionInPx = recordPosition * ROW_HEIGHT; + + scrollToPosition(positionInPx); + + setHasInitializedScroll(true); + } + } + }, [ + loading, + lastShowPageRecordId, + records, + scrollToPosition, + hasInitializedScroll, + setLastShowPageRecordId, + ]); + + useEffect(() => { + if (!loading) { + setRecordTableData({ + records, + recordGroupId, + totalCount, + }); + } + }, [records, totalCount, setRecordTableData, loading, recordGroupId]); + + useEffect(() => { + const allRecordsHaveBeenFetched = !hasNextPage; + + setHasRecordTableFetchedAllRecordsComponents(allRecordsHaveBeenFetched); + }, [hasNextPage, setHasRecordTableFetchedAllRecordsComponents]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx new file mode 100644 index 00000000000..c3579d2f20b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx @@ -0,0 +1,18 @@ +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { RecordTableRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordTableRecordGroupBodyEffects = () => { + const recordGroupDefinitions = useRecoilComponentValueV2( + recordGroupDefinitionsComponentState, + ); + + return recordGroupDefinitions.map((recordGroupDefinition) => ( + + + + )); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx new file mode 100644 index 00000000000..7f807346b50 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -0,0 +1,47 @@ +import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows'; +import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; +import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; +import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; +import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; +import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +type RecordTableRecordGroupsBodyProps = { + objectNameSingular: string; +}; + +export const RecordTableRecordGroupsBody = ({ + objectNameSingular, +}: RecordTableRecordGroupsBodyProps) => { + const tableAllRowIds = useRecoilComponentValueV2( + tableAllRowIdsComponentState, + ); + + const isRecordTableInitialLoading = useRecoilComponentValueV2( + isRecordTableInitialLoadingComponentState, + ); + + const { visibleRecordGroups } = useRecordGroups({ objectNameSingular }); + + if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { + return ; + } + + return ( + + + + {visibleRecordGroups.map((recordGroupDefinition) => ( + + + + ))} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index e119469bf50..9c28df97a98 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -78,9 +78,9 @@ const StyledTableHead = styled.thead` `; export const RecordTableHeader = ({ - objectMetadataNameSingular, + objectNameSingular, }: { - objectMetadataNameSingular: string; + objectNameSingular: string; }) => { const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, @@ -95,7 +95,7 @@ export const RecordTableHeader = ({ ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 06937919943..9b8ea55ccab 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -95,13 +95,13 @@ const StyledHeaderIcon = styled.div` export const RecordTableHeaderCell = ({ column, - objectMetadataNameSingular, + objectNameSingular, }: { column: ColumnDefinition; - objectMetadataNameSingular: string; + objectNameSingular: string; }) => { const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: objectMetadataNameSingular, + objectNameSingular, }); const resizeFieldOffsetState = useRecoilComponentCallbackStateV2( diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts index 77b3d91f0ff..470b8d9dcb8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector.ts @@ -1,7 +1,7 @@ import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus'; @@ -13,11 +13,15 @@ export const allRowsSelectedStatusComponentSelector = ({ instanceId }) => ({ get }) => { const tableRowIds = get( - tableRowIdsComponentState.atomFamily({ instanceId }), + tableAllRowIdsComponentState.atomFamily({ + instanceId, + }), ); const selectedRowIds = get( - selectedRowIdsComponentSelector.selectorFamily({ instanceId }), + selectedRowIdsComponentSelector.selectorFamily({ + instanceId, + }), ); const numberOfSelectedRows = selectedRowIds.length; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts index 9f99a82cd20..ced5cb600fc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const selectedRowIdsComponentSelector = createComponentSelectorV2< @@ -11,7 +11,11 @@ export const selectedRowIdsComponentSelector = createComponentSelectorV2< get: ({ instanceId }) => ({ get }) => { - const rowIds = get(tableRowIdsComponentState.atomFamily({ instanceId })); + const rowIds = get( + tableAllRowIdsComponentState.atomFamily({ + instanceId, + }), + ); return rowIds.filter( (rowId) => diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts index 3f1a8c99732..1579e41b06c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const unselectedRowIdsComponentSelector = createComponentSelectorV2< @@ -11,7 +11,11 @@ export const unselectedRowIdsComponentSelector = createComponentSelectorV2< get: ({ instanceId }) => ({ get }) => { - const rowIds = get(tableRowIdsComponentState.atomFamily({ instanceId })); + const rowIds = get( + tableAllRowIdsComponentState.atomFamily({ + instanceId, + }), + ); return rowIds.filter( (rowId) => diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts similarity index 73% rename from packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts index c9198979153..e6f8ef4b24a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts @@ -1,8 +1,8 @@ import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const tableRowIdsComponentState = createComponentStateV2({ - key: 'tableRowIdsComponentState', +export const tableAllRowIdsComponentState = createComponentStateV2({ + key: 'tableAllRowIdsComponentState', defaultValue: [], componentInstanceContext: RecordTableComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableRecordGroupIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableRecordGroupIdsComponentState.ts new file mode 100644 index 00000000000..fd6f666a948 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/tableRecordGroupIdsComponentState.ts @@ -0,0 +1,11 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const tableRecordGroupIdsComponentState = createComponentStateV2< + RecordGroupDefinition['id'][] +>({ + key: 'tableRecordGroupIdsComponentState', + defaultValue: [], + componentInstanceContext: RecordTableComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts new file mode 100644 index 00000000000..395f7f185bb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; + +export const tableRowIdsByGroupComponentFamilyState = + createComponentFamilyStateV2({ + key: 'tableRowIdsByGroupComponentFamilyState', + defaultValue: [], + componentInstanceContext: RecordTableComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2.ts index 04c9a2d2a8b..8662ca1e8c0 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ import { selectorFamily, SerializableParam } from 'recoil'; import { ComponentFamilyReadOnlySelectorV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyReadOnlySelectorV2'; @@ -9,7 +11,26 @@ import { SelectorGetter } from '@/ui/utilities/state/types/SelectorGetter'; import { SelectorSetter } from '@/ui/utilities/state/types/SelectorSetter'; import { isDefined } from 'twenty-ui'; -export const createComponentFamilySelectorV2 = < +export function createComponentFamilySelectorV2< + ValueType, + FamilyKey extends SerializableParam, +>(options: { + key: string; + get: SelectorGetter>; + componentInstanceContext: ComponentInstanceStateContext | null; +}): ComponentFamilySelectorV2; + +export function createComponentFamilySelectorV2< + ValueType, + FamilyKey extends SerializableParam, +>(options: { + key: string; + get: SelectorGetter>; + set: SelectorSetter>; + componentInstanceContext: ComponentInstanceStateContext | null; +}): ComponentFamilyReadOnlySelectorV2; + +export function createComponentFamilySelectorV2< ValueType, FamilyKey extends SerializableParam, >({ @@ -24,7 +45,7 @@ export const createComponentFamilySelectorV2 = < componentInstanceContext: ComponentInstanceStateContext | null; }): | ComponentFamilySelectorV2 - | ComponentFamilyReadOnlySelectorV2 => { + | ComponentFamilyReadOnlySelectorV2 { if (isDefined(componentInstanceContext)) { globalComponentInstanceContextMap.set(key, componentInstanceContext); } @@ -55,4 +76,4 @@ export const createComponentFamilySelectorV2 = < }), } satisfies ComponentFamilyReadOnlySelectorV2; } -}; +} diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilyStateV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilyStateV2.ts index 2d1f42e61fd..f314c2c4a69 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilyStateV2.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentFamilyStateV2.ts @@ -6,11 +6,18 @@ import { AtomEffect, atomFamily, SerializableParam } from 'recoil'; import { isDefined } from 'twenty-ui'; -type CreateComponentFamilyStateArgs = { +type CreateComponentFamilyStateArgs< + ValueType, + FamilyKey extends SerializableParam, +> = { key: string; defaultValue: ValueType; componentInstanceContext: ComponentInstanceStateContext | null; - effects?: AtomEffect[]; + effects?: + | AtomEffect[] + | (( + param: ComponentFamilyStateKeyV2, + ) => ReadonlyArray>); }; export const createComponentFamilyStateV2 = < @@ -21,10 +28,10 @@ export const createComponentFamilyStateV2 = < effects, defaultValue, componentInstanceContext, -}: CreateComponentFamilyStateArgs): ComponentFamilyStateV2< +}: CreateComponentFamilyStateArgs< ValueType, FamilyKey -> => { +>): ComponentFamilyStateV2 => { if (isDefined(componentInstanceContext)) { globalComponentInstanceContextMap.set(key, componentInstanceContext); }