feat: record group fetch more (#8868)

Fix #8756 

This PR is adding a load more button when we're grouping by a field in
table view.
Only 8 records are printed at first, and a click on `Load more` is
needed to show more records.

<img width="1347" alt="Screenshot 2024-12-04 at 11 54 15 AM"
src="https://github.com/user-attachments/assets/4ad6ad4f-8de9-424d-b7b6-5f82f28c53df">
<img width="1352" alt="Screenshot 2024-12-04 at 11 54 23 AM"
src="https://github.com/user-attachments/assets/2a94b4ac-7285-4ba2-9cff-d2f653e36302">

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2024-12-05 10:51:56 +01:00
committed by GitHub
parent b686e0b2e6
commit 7ab00a4c82
19 changed files with 789 additions and 165 deletions

View File

@ -0,0 +1,467 @@
import { expect } from '@storybook/test';
import { renderHook } from '@testing-library/react';
import { ReactNode, act } from 'react';
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { MockedResponse } from '@apollo/client/testing';
import gql from 'graphql-tag';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { getPeopleMock } from '~/testing/mock-data/people';
const recordTableId = 'people';
const objectNameSingular = 'person';
const onColumnsChange = jest.fn();
const ObjectNamePluralSetter = ({ children }: { children: ReactNode }) => {
return <>{children}</>;
};
const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindManyPeople(
$filter: PersonFilterInput
$orderBy: [PersonOrderByInput]
$lastCursor: String
$limit: Int
) {
people(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
avatarUrl
deletedAt
id
name {
firstName
lastName
}
noteTargets {
edges {
node {
__typename
company {
__typename
accountOwnerId
address {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
annualRecurringRevenue {
amountMicros
currencyCode
}
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
position
tagline
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
companyId
createdAt
deletedAt
id
note {
__typename
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
position
title
updatedAt
}
noteId
opportunity {
__typename
amount {
amountMicros
currencyCode
}
closeDate
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
pointOfContactId
position
stage
updatedAt
}
opportunityId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
personId
rocket {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
position
updatedAt
}
rocketId
updatedAt
}
}
}
position
taskTargets {
edges {
node {
__typename
company {
__typename
accountOwnerId
address {
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}
annualRecurringRevenue {
amountMicros
currencyCode
}
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
position
tagline
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
companyId
createdAt
deletedAt
id
opportunity {
__typename
amount {
amountMicros
currencyCode
}
closeDate
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
pointOfContactId
position
stage
updatedAt
}
opportunityId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
personId
rocket {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
position
updatedAt
}
rocketId
task {
__typename
assigneeId
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
dueAt
id
position
status
title
updatedAt
}
taskId
updatedAt
}
}
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: {},
orderBy: [{ position: 'AscNullsFirst' }],
},
},
result: jest.fn(() => ({
data: {
people: getPeopleMock(),
},
})),
},
];
const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks,
});
const Wrapper = ({ children }: { children: ReactNode }) => {
return (
<HookMockWrapper>
<ObjectNamePluralSetter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={onColumnsChange}
>
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}>
{children}
</RecordGroupContext.Provider>
</RecordTableComponentInstance>
</ViewComponentInstanceContext.Provider>
</ObjectNamePluralSetter>
</HookMockWrapper>
);
};
describe('useLazyLoadRecordIndexTable', () => {
it('should fetch', async () => {
const { result } = renderHook(
() => {
const { findManyRecords, ...result } =
useLazyLoadRecordIndexTable(objectNameSingular);
return {
findManyRecords,
...result,
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.loading).toBe(false);
act(() => {
result.current.findManyRecords();
});
expect(Array.isArray(result.current.records)).toBe(true);
expect(result.current.records.length).toBe(13);
});
});

View File

@ -1,58 +0,0 @@
import { expect } from '@storybook/test';
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';
const objectNameSingular = 'person';
const onColumnsChange = jest.fn();
const ObjectNamePluralSetter = ({ children }: { children: ReactNode }) => {
return <>{children}</>;
};
const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks,
});
const Wrapper = ({ children }: { children: ReactNode }) => {
return (
<HookMockWrapper>
<ObjectNamePluralSetter>
<ViewComponentInstanceContext.Provider
value={{ instanceId: 'instanceId' }}
>
<RecordTableComponentInstance
recordTableId={recordTableId}
onColumnsChange={onColumnsChange}
>
<RecordGroupContext.Provider value={{ recordGroupId: 'default' }}>
{children}
</RecordGroupContext.Provider>
</RecordTableComponentInstance>
</ViewComponentInstanceContext.Provider>
</ObjectNamePluralSetter>
</HookMockWrapper>
);
};
describe('useObjectRecordTable', () => {
it('should skip fetch if currentWorkspace is undefined', async () => {
const { result } = renderHook(
() => useLoadRecordIndexTable(objectNameSingular),
{
wrapper: Wrapper,
},
);
expect(result.current.loading).toBe(false);
expect(Array.isArray(result.current.records)).toBe(true);
expect(result.current.records.length).toBe(13);
});
});

View File

@ -142,7 +142,7 @@ export const useFetchMoreRecordsWithPagination = <
const pageInfo =
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
if (isDefined(data?.[objectMetadataItem.namePlural])) {
if (isDefined(pageInfo)) {
set(
cursorFamilyState(queryIdentifier),
pageInfo.endCursor ?? '',
@ -201,7 +201,6 @@ export const useFetchMoreRecordsWithPagination = <
fetchMore,
filter,
orderBy,
data,
onCompleted,
handleFindManyRecordsError,
queryIdentifier,

View File

@ -67,7 +67,7 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onError: handleFindManyRecordsError,
});
const { fetchMoreRecords, totalCount, records } =
const { fetchMoreRecords, totalCount, records, hasNextPage } =
useFetchMoreRecordsWithPagination<T>({
objectNameSingular,
filter,
@ -108,8 +108,9 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
loading,
error,
fetchMore,
fetchMoreRecordsWithPagination: fetchMoreRecords,
fetchMoreRecords,
queryStateIdentifier: queryIdentifier,
findManyRecords: findManyRecordsLazy,
hasNextPage,
};
};

View File

@ -2,7 +2,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { sortedInsert } from '@/object-record/record-group/utils/sortedInsert';
import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
@ -54,7 +54,7 @@ export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2<
isDefined(recordGroupDefinition) &&
recordGroupDefinition.isVisible
) {
sortedInsert(result, recordGroupDefinition, comparator);
recordGroupSortedInsert(result, recordGroupDefinition, comparator);
}
}

View File

@ -0,0 +1,66 @@
// export const recordGroupSortedInsert = <T>(
// array: T[],
// item: T,
// comparator: (a: T, b: T) => number,
// ) => {
// let low = 0;
// let high = array.length;
import { expect } from '@storybook/test';
// while (low < high) {
// const mid = Math.floor((low + high) / 2);
// if (comparator(item, array[mid]) < 0) {
// high = mid;
// } else {
// low = mid + 1;
// }
// }
// array.splice(low, 0, item);
// };
import { recordGroupSortedInsert } from '../recordGroupSortedInsert';
describe('recordGroupSortedInsert', () => {
it('should insert an item into an empty array', () => {
const array: number[] = [];
const item = 1;
const comparator = (a: number, b: number) => a - b;
recordGroupSortedInsert(array, item, comparator);
expect(array).toEqual([1]);
});
it('should insert an item at the beginning of the array', () => {
const array = [2, 3, 4];
const item = 1;
const comparator = (a: number, b: number) => a - b;
recordGroupSortedInsert(array, item, comparator);
expect(array).toEqual([1, 2, 3, 4]);
});
it('should insert an item at the end of the array', () => {
const array = [1, 2, 3];
const item = 4;
const comparator = (a: number, b: number) => a - b;
recordGroupSortedInsert(array, item, comparator);
expect(array).toEqual([1, 2, 3, 4]);
});
it('should insert an item in the middle of the array', () => {
const array = [1, 3, 4];
const item = 2;
const comparator = (a: number, b: number) => a - b;
recordGroupSortedInsert(array, item, comparator);
expect(array).toEqual([1, 2, 3, 4]);
});
});

View File

@ -1,4 +1,4 @@
export const sortedInsert = <T>(
export const recordGroupSortedInsert = <T>(
array: T[],
item: T,
comparator: (a: T, b: T) => number,

View File

@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useContext, useEffect } from 'react';
@ -31,7 +31,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
objectNameSingular,
});
const findManyRecordsParams = useFindManyParams(
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem?.nameSingular ?? '',
objectMetadataItem?.namePlural ?? '',
);

View File

@ -13,7 +13,7 @@ import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRec
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize';
import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
@ -94,29 +94,24 @@ export const useExportFetchRecords = ({
objectMetadataItem,
);
const findManyRecordsParams = useFindManyParams(
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
recordIndexId,
);
const {
findManyRecords,
totalCount,
records,
fetchMoreRecordsWithPagination,
loading,
} = useLazyFindManyRecords({
...findManyRecordsParams,
filter: queryFilter,
limit: pageSize,
});
const { findManyRecords, totalCount, records, fetchMoreRecords, loading } =
useLazyFindManyRecords({
...findManyRecordsParams,
filter: queryFilter,
limit: pageSize,
});
useEffect(() => {
const fetchNextPage = async () => {
setInflight(true);
setPreviousRecordCount(records.length);
await fetchMoreRecordsWithPagination();
await fetchMoreRecords();
setPageCount((state) => state + 1);
setProgress({
@ -166,7 +161,7 @@ export const useExportFetchRecords = ({
}
}, [
delayMs,
fetchMoreRecordsWithPagination,
fetchMoreRecords,
inflight,
isDownloading,
pageCount,

View File

@ -1,25 +1,15 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { 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';
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
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 = (
export const useFindManyRecordIndexTableParams = (
objectNameSingular: string,
recordTableId?: string,
) => {
@ -84,51 +74,7 @@ export const useFindManyParams = (
...recordGroupFilter,
},
orderBy,
};
};
export const useLoadRecordIndexTable = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { setRecordTableData, setIsRecordTableInitialLoading } =
useRecordTable();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const params = useFindManyParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const {
records,
loading,
totalCount,
fetchMoreRecords,
queryStateIdentifier,
hasNextPage,
} = useFindManyRecords({
...params,
recordGqlFields,
onCompleted: () => {
setIsRecordTableInitialLoading(false);
},
onError: () => {
setIsRecordTableInitialLoading(false);
},
skip: isNull(currentWorkspaceMember),
});
return {
records:
currentWorkspace?.activationStatus === WorkspaceActivationStatus.Active
? records
: SIGN_IN_BACKGROUND_MOCK_COMPANIES,
totalCount: totalCount,
loading,
fetchMoreRecords,
queryStateIdentifier,
setRecordTableData,
hasNextPage,
// If we have a current record group definition, we only want to fetch 8 records by page
...(currentRecordGroupDefinition ? { limit: 8 } : {}),
};
};

View File

@ -0,0 +1,58 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies';
import { WorkspaceActivationStatus } from '~/generated/graphql';
export const useLazyLoadRecordIndexTable = (objectNameSingular: string) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { setRecordTableData, setIsRecordTableInitialLoading } =
useRecordTable();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const params = useFindManyRecordIndexTableParams(objectNameSingular);
const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem });
const {
findManyRecords,
records,
loading,
totalCount,
fetchMoreRecords,
queryStateIdentifier,
hasNextPage,
} = useLazyFindManyRecords({
...params,
recordGqlFields,
onCompleted: () => {
setIsRecordTableInitialLoading(false);
},
onError: () => {
setIsRecordTableInitialLoading(false);
},
});
return {
findManyRecords,
records:
currentWorkspace?.activationStatus === WorkspaceActivationStatus.Active
? records
: SIGN_IN_BACKGROUND_MOCK_COMPANIES,
totalCount: totalCount,
loading,
fetchMoreRecords,
queryStateIdentifier,
setRecordTableData,
hasNextPage,
};
};

View File

@ -0,0 +1,10 @@
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
export const recordIndexHasFetchedAllRecordsByGroupComponentState =
createComponentFamilyStateV2<boolean, RecordGroupDefinition['id']>({
key: 'recordIndexHasFetchedAllRecordsByGroupComponentState',
componentInstanceContext: ViewComponentInstanceContext,
defaultValue: false,
});

View File

@ -2,26 +2,30 @@ import { useContext, useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
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 { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
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 { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
export const RecordTableNoRecordGroupBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
const {
findManyRecords,
fetchMoreRecords,
records,
totalCount,
@ -29,7 +33,7 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
loading,
queryStateIdentifier,
hasNextPage,
} = useLoadRecordIndexTable(objectNameSingular);
} = useLazyLoadRecordIndexTable(objectNameSingular);
const isFetchingMoreObjects = useRecoilValue(
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
@ -132,5 +136,13 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
setEncounteredUnrecoverableError,
]);
useEffect(() => {
if (isNull(currentWorkspaceMember)) {
return;
}
findManyRecords();
}, [currentWorkspaceMember, findManyRecords]);
return <></>;
};

View File

@ -1,29 +1,39 @@
import { useContext, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
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 { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
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 { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const recordGroupId = useCurrentRecordGroupId();
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
const { records, totalCount, setRecordTableData, loading, hasNextPage } =
useLoadRecordIndexTable(objectNameSingular);
const {
findManyRecords,
records,
totalCount,
setRecordTableData,
loading,
hasNextPage,
} = useLazyLoadRecordIndexTable(objectNameSingular);
const setHasRecordTableFetchedAllRecordsComponents =
useSetRecoilComponentStateV2(
hasRecordTableFetchedAllRecordsComponentStateV2,
const setHasRecordFetchedAllRecordsComponents =
useSetRecoilComponentFamilyStateV2(
recordIndexHasFetchedAllRecordsByGroupComponentState,
recordGroupId,
);
const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState);
@ -65,8 +75,16 @@ export const RecordTableRecordGroupBodyEffect = () => {
useEffect(() => {
const allRecordsHaveBeenFetched = !hasNextPage;
setHasRecordTableFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
}, [hasNextPage, setHasRecordTableFetchedAllRecordsComponents]);
setHasRecordFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
}, [hasNextPage, setHasRecordFetchedAllRecordsComponents]);
useEffect(() => {
if (isNull(currentWorkspaceMember)) {
return;
}
findManyRecords();
}, [currentWorkspaceMember, findManyRecords]);
return <></>;
};

View File

@ -7,6 +7,7 @@ import { RecordTableBodyLoading } from '@/object-record/record-table/record-tabl
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -40,6 +41,7 @@ export const RecordTableRecordGroupsBody = () => {
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
<RecordTableRecordGroupSection />
<RecordTableRecordGroupRows />
<RecordTableRecordGroupSectionLoadMore />
</RecordTableBodyDroppable>
</RecordGroupContext.Provider>
))}

View File

@ -61,6 +61,7 @@ export const RecordTableTd = ({
hasRightBorder = true,
hasBottomBorder = true,
width,
colSpan,
...dragHandleProps
}: {
className?: string;
@ -74,6 +75,7 @@ export const RecordTableTd = ({
hasBottomBorder?: boolean;
left?: number;
width?: number;
colSpan?: number;
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
const { theme } = useContext(ThemeContext);
@ -97,6 +99,7 @@ export const RecordTableTd = ({
hasRightBorder={hasRightBorder}
hasBottomBorder={hasBottomBorder}
width={width}
colSpan={colSpan}
// eslint-disable-next-line react/jsx-props-no-spreading
{...dragHandleProps}
>

View File

@ -0,0 +1,68 @@
import styled from '@emotion/styled';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTheme } from '@emotion/react';
import { IconComponent } from 'twenty-ui';
const StyledTrContainer = styled.tr`
cursor: pointer;
`;
const StyledIconContainer = styled(RecordTableTd)`
border-right: none;
color: ${({ theme }) => theme.font.color.secondary};
text-align: center;
vertical-align: middle;
padding-top: 3px;
`;
const StyledRecordTableTdTextContainer = styled(RecordTableTd)`
border-right: none;
height: 32px;
`;
const StyledText = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
margin-left: ${({ theme }) => theme.spacing(2)};
font-size: ${({ theme }) => theme.font.size.md};
text-align: center;
vertical-align: middle;
`;
const StyledEmptyTd = styled.td`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
type RecordTableActionRowProps = {
LeftIcon: IconComponent;
text: string;
onClick?: (event?: React.MouseEvent<HTMLTableRowElement>) => void;
};
export const RecordTableActionRow = ({
LeftIcon,
text,
onClick,
}: RecordTableActionRowProps) => {
const theme = useTheme();
const visibleColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
return (
<StyledTrContainer onClick={onClick}>
<td aria-hidden />
<StyledIconContainer>
<LeftIcon size={theme.icon.size.sm} color={theme.font.color.tertiary} />
</StyledIconContainer>
<StyledRecordTableTdTextContainer colSpan={visibleColumns.length}>
<StyledText>{text}</StyledText>
</StyledRecordTableTdTextContainer>
<StyledEmptyTd />
<StyledEmptyTd />
</StyledTrContainer>
);
};

View File

@ -8,6 +8,7 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
@ -19,8 +20,8 @@ const StyledTrContainer = styled.tr`
cursor: pointer;
`;
const StyledChevronContainer = styled.td`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
const StyledChevronContainer = styled(RecordTableTd)`
border-right: none;
color: ${({ theme }) => theme.font.color.secondary};
text-align: center;
vertical-align: middle;
@ -33,10 +34,9 @@ const StyledTotalRow = styled.span`
vertical-align: middle;
`;
const StyledRecordGroupSection = styled.td`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
padding-bottom: 6px;
padding-top: 6px;
const StyledRecordGroupSection = styled(RecordTableTd)`
border-right: none;
height: 32px;
`;
const StyledEmptyTd = styled.td`
@ -79,7 +79,7 @@ export const RecordTableRecordGroupSection = () => {
return (
<StyledTrContainer onClick={handleDropdownToggle}>
<td aria-hidden></td>
<td aria-hidden />
<StyledChevronContainer>
<motion.span
animate={{ rotate: isRecordGroupTableSectionToggled ? 180 : 0 }}
@ -108,8 +108,8 @@ export const RecordTableRecordGroupSection = () => {
/>
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
</StyledRecordGroupSection>
<StyledEmptyTd></StyledEmptyTd>
<StyledEmptyTd></StyledEmptyTd>
<StyledEmptyTd />
<StyledEmptyTd />
</StyledTrContainer>
);
};

View File

@ -0,0 +1,37 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useContext } from 'react';
import { IconArrowDown } from 'twenty-ui';
export const RecordTableRecordGroupSectionLoadMore = () => {
const { objectNameSingular } = useContext(RecordTableContext);
const currentRecordGroupId = useCurrentRecordGroupId();
const { fetchMoreRecords } = useLazyLoadRecordIndexTable(objectNameSingular);
const hasFetchedAllRecords = useRecoilComponentFamilyValueV2(
recordIndexHasFetchedAllRecordsByGroupComponentState,
currentRecordGroupId,
);
const handleLoadMore = () => {
fetchMoreRecords();
};
if (hasFetchedAllRecords) {
return null;
}
return (
<RecordTableActionRow
LeftIcon={IconArrowDown}
text="Load more"
onClick={handleLoadMore}
/>
);
};