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 58828525ea..7008bec924 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 0000000000..78afc40337 --- /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 0000000000..b9960a19c6 --- /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 97151a5838..b0c738a0fb 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 0000000000..402d8743de --- /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 0000000000..bccab902cc --- /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 a2b0e03480..22f2043cf4 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 485f4b0498..ac2fd976d2 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 0000000000..5ed2670813 --- /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 0000000000..7b28090002 --- /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 8780f623ab..82b51a2e16 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 38b8e75c21..24d54fb1ba 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 4b23ac749b..7a35ef87a6 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 c6ef772521..7694d5f757 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 64b33fb374..0bcc4ac78c 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 8084985a2d..f2a765d6d4 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 f66b6643d3..76a29fccab 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 0000000000..831f1dc4fd --- /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 0000000000..c3579d2f20 --- /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 0000000000..7f807346b5 --- /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 e119469bf5..9c28df97a9 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 0693791994..9b8ea55cca 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 77b3d91f0f..470b8d9dcb 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 9f99a82cd2..ced5cb600f 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 3f1a8c9973..1579e41b06 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 c919897915..e6f8ef4b24 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 0000000000..fd6f666a94 --- /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 0000000000..395f7f185b --- /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 04c9a2d2a8..8662ca1e8c 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 2d1f42e61f..f314c2c4a6 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); }