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:
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -142,7 +142,7 @@ export const useFetchMoreRecordsWithPagination = <
|
|||||||
const pageInfo =
|
const pageInfo =
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
|
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
|
||||||
|
|
||||||
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
if (isDefined(pageInfo)) {
|
||||||
set(
|
set(
|
||||||
cursorFamilyState(queryIdentifier),
|
cursorFamilyState(queryIdentifier),
|
||||||
pageInfo.endCursor ?? '',
|
pageInfo.endCursor ?? '',
|
||||||
@ -201,7 +201,6 @@ export const useFetchMoreRecordsWithPagination = <
|
|||||||
fetchMore,
|
fetchMore,
|
||||||
filter,
|
filter,
|
||||||
orderBy,
|
orderBy,
|
||||||
data,
|
|
||||||
onCompleted,
|
onCompleted,
|
||||||
handleFindManyRecordsError,
|
handleFindManyRecordsError,
|
||||||
queryIdentifier,
|
queryIdentifier,
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
onError: handleFindManyRecordsError,
|
onError: handleFindManyRecordsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fetchMoreRecords, totalCount, records } =
|
const { fetchMoreRecords, totalCount, records, hasNextPage } =
|
||||||
useFetchMoreRecordsWithPagination<T>({
|
useFetchMoreRecordsWithPagination<T>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter,
|
filter,
|
||||||
@ -108,8 +108,9 @@ export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
fetchMore,
|
fetchMore,
|
||||||
fetchMoreRecordsWithPagination: fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
queryStateIdentifier: queryIdentifier,
|
queryStateIdentifier: queryIdentifier,
|
||||||
findManyRecords: findManyRecordsLazy,
|
findManyRecords: findManyRecordsLazy,
|
||||||
|
hasNextPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
|
|||||||
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
|
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
|
||||||
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
|
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 { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
|
||||||
|
|
||||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||||
@ -54,7 +54,7 @@ export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2<
|
|||||||
isDefined(recordGroupDefinition) &&
|
isDefined(recordGroupDefinition) &&
|
||||||
recordGroupDefinition.isVisible
|
recordGroupDefinition.isVisible
|
||||||
) {
|
) {
|
||||||
sortedInsert(result, recordGroupDefinition, comparator);
|
recordGroupSortedInsert(result, recordGroupDefinition, comparator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export const sortedInsert = <T>(
|
export const recordGroupSortedInsert = <T>(
|
||||||
array: T[],
|
array: T[],
|
||||||
item: T,
|
item: T,
|
||||||
comparator: (a: T, b: T) => number,
|
comparator: (a: T, b: T) => number,
|
||||||
@ -6,7 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
|||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
@ -31,7 +31,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const findManyRecordsParams = useFindManyParams(
|
const findManyRecordsParams = useFindManyRecordIndexTableParams(
|
||||||
objectMetadataItem?.nameSingular ?? '',
|
objectMetadataItem?.nameSingular ?? '',
|
||||||
objectMetadataItem?.namePlural ?? '',
|
objectMetadataItem?.namePlural ?? '',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { 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 { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard';
|
||||||
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
|
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 { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
@ -94,29 +94,24 @@ export const useExportFetchRecords = ({
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
);
|
);
|
||||||
|
|
||||||
const findManyRecordsParams = useFindManyParams(
|
const findManyRecordsParams = useFindManyRecordIndexTableParams(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
recordIndexId,
|
recordIndexId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { findManyRecords, totalCount, records, fetchMoreRecords, loading } =
|
||||||
findManyRecords,
|
useLazyFindManyRecords({
|
||||||
totalCount,
|
...findManyRecordsParams,
|
||||||
records,
|
filter: queryFilter,
|
||||||
fetchMoreRecordsWithPagination,
|
limit: pageSize,
|
||||||
loading,
|
});
|
||||||
} = useLazyFindManyRecords({
|
|
||||||
...findManyRecordsParams,
|
|
||||||
filter: queryFilter,
|
|
||||||
limit: pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchNextPage = async () => {
|
const fetchNextPage = async () => {
|
||||||
setInflight(true);
|
setInflight(true);
|
||||||
setPreviousRecordCount(records.length);
|
setPreviousRecordCount(records.length);
|
||||||
|
|
||||||
await fetchMoreRecordsWithPagination();
|
await fetchMoreRecords();
|
||||||
|
|
||||||
setPageCount((state) => state + 1);
|
setPageCount((state) => state + 1);
|
||||||
setProgress({
|
setProgress({
|
||||||
@ -166,7 +161,7 @@ export const useExportFetchRecords = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
delayMs,
|
delayMs,
|
||||||
fetchMoreRecordsWithPagination,
|
fetchMoreRecords,
|
||||||
inflight,
|
inflight,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
pageCount,
|
pageCount,
|
||||||
|
|||||||
@ -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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||||
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
|
||||||
import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
|
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 { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
|
||||||
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
|
import { tableSortsComponentState } from '@/object-record/record-table/states/tableSortsComponentState';
|
||||||
import { tableViewFilterGroupsComponentState } from '@/object-record/record-table/states/tableViewFilterGroupsComponentState';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { isNull } from '@sniptt/guards';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
import { WorkspaceActivationStatus } from '~/generated/graphql';
|
|
||||||
|
|
||||||
export const useFindManyParams = (
|
export const useFindManyRecordIndexTableParams = (
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
recordTableId?: string,
|
recordTableId?: string,
|
||||||
) => {
|
) => {
|
||||||
@ -84,51 +74,7 @@ export const useFindManyParams = (
|
|||||||
...recordGroupFilter,
|
...recordGroupFilter,
|
||||||
},
|
},
|
||||||
orderBy,
|
orderBy,
|
||||||
};
|
// If we have a current record group definition, we only want to fetch 8 records by page
|
||||||
};
|
...(currentRecordGroupDefinition ? { limit: 8 } : {}),
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
});
|
||||||
@ -2,26 +2,30 @@ import { useContext, useEffect, useState } from 'react';
|
|||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
|
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 { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
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 { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
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 { 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 = () => {
|
export const RecordTableNoRecordGroupBodyEffect = () => {
|
||||||
const { objectNameSingular } = useContext(RecordTableContext);
|
const { objectNameSingular } = useContext(RecordTableContext);
|
||||||
|
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
findManyRecords,
|
||||||
fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
records,
|
records,
|
||||||
totalCount,
|
totalCount,
|
||||||
@ -29,7 +33,7 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
|
|||||||
loading,
|
loading,
|
||||||
queryStateIdentifier,
|
queryStateIdentifier,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
} = useLoadRecordIndexTable(objectNameSingular);
|
} = useLazyLoadRecordIndexTable(objectNameSingular);
|
||||||
|
|
||||||
const isFetchingMoreObjects = useRecoilValue(
|
const isFetchingMoreObjects = useRecoilValue(
|
||||||
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
|
isFetchingMoreRecordsFamilyState(queryStateIdentifier),
|
||||||
@ -132,5 +136,13 @@ export const RecordTableNoRecordGroupBodyEffect = () => {
|
|||||||
setEncounteredUnrecoverableError,
|
setEncounteredUnrecoverableError,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNull(currentWorkspaceMember)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
findManyRecords();
|
||||||
|
}, [currentWorkspaceMember, findManyRecords]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,29 +1,39 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
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 { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
|
||||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
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 { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
|
||||||
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
|
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
|
||||||
|
|
||||||
export const RecordTableRecordGroupBodyEffect = () => {
|
export const RecordTableRecordGroupBodyEffect = () => {
|
||||||
const { objectNameSingular } = useContext(RecordTableContext);
|
const { objectNameSingular } = useContext(RecordTableContext);
|
||||||
|
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
const recordGroupId = useCurrentRecordGroupId();
|
const recordGroupId = useCurrentRecordGroupId();
|
||||||
|
|
||||||
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
const [hasInitializedScroll, setHasInitializedScroll] = useState(false);
|
||||||
|
|
||||||
const { records, totalCount, setRecordTableData, loading, hasNextPage } =
|
const {
|
||||||
useLoadRecordIndexTable(objectNameSingular);
|
findManyRecords,
|
||||||
|
records,
|
||||||
|
totalCount,
|
||||||
|
setRecordTableData,
|
||||||
|
loading,
|
||||||
|
hasNextPage,
|
||||||
|
} = useLazyLoadRecordIndexTable(objectNameSingular);
|
||||||
|
|
||||||
const setHasRecordTableFetchedAllRecordsComponents =
|
const setHasRecordFetchedAllRecordsComponents =
|
||||||
useSetRecoilComponentStateV2(
|
useSetRecoilComponentFamilyStateV2(
|
||||||
hasRecordTableFetchedAllRecordsComponentStateV2,
|
recordIndexHasFetchedAllRecordsByGroupComponentState,
|
||||||
|
recordGroupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState);
|
const [lastShowPageRecordId] = useRecoilState(lastShowPageRecordIdState);
|
||||||
@ -65,8 +75,16 @@ export const RecordTableRecordGroupBodyEffect = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const allRecordsHaveBeenFetched = !hasNextPage;
|
const allRecordsHaveBeenFetched = !hasNextPage;
|
||||||
|
|
||||||
setHasRecordTableFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
|
setHasRecordFetchedAllRecordsComponents(allRecordsHaveBeenFetched);
|
||||||
}, [hasNextPage, setHasRecordTableFetchedAllRecordsComponents]);
|
}, [hasNextPage, setHasRecordFetchedAllRecordsComponents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNull(currentWorkspaceMember)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
findManyRecords();
|
||||||
|
}, [currentWorkspaceMember, findManyRecords]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
||||||
import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
|
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 { 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 { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ export const RecordTableRecordGroupsBody = () => {
|
|||||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||||
<RecordTableRecordGroupSection />
|
<RecordTableRecordGroupSection />
|
||||||
<RecordTableRecordGroupRows />
|
<RecordTableRecordGroupRows />
|
||||||
|
<RecordTableRecordGroupSectionLoadMore />
|
||||||
</RecordTableBodyDroppable>
|
</RecordTableBodyDroppable>
|
||||||
</RecordGroupContext.Provider>
|
</RecordGroupContext.Provider>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const RecordTableTd = ({
|
|||||||
hasRightBorder = true,
|
hasRightBorder = true,
|
||||||
hasBottomBorder = true,
|
hasBottomBorder = true,
|
||||||
width,
|
width,
|
||||||
|
colSpan,
|
||||||
...dragHandleProps
|
...dragHandleProps
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -74,6 +75,7 @@ export const RecordTableTd = ({
|
|||||||
hasBottomBorder?: boolean;
|
hasBottomBorder?: boolean;
|
||||||
left?: number;
|
left?: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
|
colSpan?: number;
|
||||||
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
|
} & (Partial<DraggableProvidedDragHandleProps> | null)) => {
|
||||||
const { theme } = useContext(ThemeContext);
|
const { theme } = useContext(ThemeContext);
|
||||||
|
|
||||||
@ -97,6 +99,7 @@ export const RecordTableTd = ({
|
|||||||
hasRightBorder={hasRightBorder}
|
hasRightBorder={hasRightBorder}
|
||||||
hasBottomBorder={hasBottomBorder}
|
hasBottomBorder={hasBottomBorder}
|
||||||
width={width}
|
width={width}
|
||||||
|
colSpan={colSpan}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...dragHandleProps}
|
{...dragHandleProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC
|
|||||||
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
|
||||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
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 { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
@ -19,8 +20,8 @@ const StyledTrContainer = styled.tr`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledChevronContainer = styled.td`
|
const StyledChevronContainer = styled(RecordTableTd)`
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
border-right: none;
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@ -33,10 +34,9 @@ const StyledTotalRow = styled.span`
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledRecordGroupSection = styled.td`
|
const StyledRecordGroupSection = styled(RecordTableTd)`
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
border-right: none;
|
||||||
padding-bottom: 6px;
|
height: 32px;
|
||||||
padding-top: 6px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEmptyTd = styled.td`
|
const StyledEmptyTd = styled.td`
|
||||||
@ -79,7 +79,7 @@ export const RecordTableRecordGroupSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTrContainer onClick={handleDropdownToggle}>
|
<StyledTrContainer onClick={handleDropdownToggle}>
|
||||||
<td aria-hidden></td>
|
<td aria-hidden />
|
||||||
<StyledChevronContainer>
|
<StyledChevronContainer>
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={{ rotate: isRecordGroupTableSectionToggled ? 180 : 0 }}
|
animate={{ rotate: isRecordGroupTableSectionToggled ? 180 : 0 }}
|
||||||
@ -108,8 +108,8 @@ export const RecordTableRecordGroupSection = () => {
|
|||||||
/>
|
/>
|
||||||
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
|
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
|
||||||
</StyledRecordGroupSection>
|
</StyledRecordGroupSection>
|
||||||
<StyledEmptyTd></StyledEmptyTd>
|
<StyledEmptyTd />
|
||||||
<StyledEmptyTd></StyledEmptyTd>
|
<StyledEmptyTd />
|
||||||
</StyledTrContainer>
|
</StyledTrContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user