☑️ Refacto "Select All/Unselect all" on indexes (#5320)
### Description - Refacto "Select All/Unselect all" on indexes - Add sequential mass deletion from front end (limited to 10k records) - Fixed coverage with new unit tests on new useFetchAllRecordIds hook and other utils ### Refs Closes #4397 Closes #5169 ### Demo https://github.com/twentyhq/twenty/assets/26528466/2658ad2c-827e-4670-b42b-3092e268ff32 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Toledodev <rafael.toledo@engenharia.ufjf.br> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -0,0 +1,17 @@
|
|||||||
|
import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName';
|
||||||
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
|
|
||||||
|
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||||
|
|
||||||
|
describe('getObjectMetadataItemBySingularName', () => {
|
||||||
|
it('should work as expected', () => {
|
||||||
|
const firstObjectMetadataItem = mockObjectMetadataItems[0];
|
||||||
|
|
||||||
|
const foundObjectMetadataItem = getObjectMetadataItemByNameSingular({
|
||||||
|
objectMetadataItems: mockObjectMetadataItems,
|
||||||
|
objectNameSingular: firstObjectMetadataItem.nameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(foundObjectMetadataItem.id).toEqual(firstObjectMetadataItem.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { peopleQueryResult } from '~/testing/mock-data/people';
|
||||||
|
|
||||||
|
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||||
|
|
||||||
|
describe('isObjectRecordConnection', () => {
|
||||||
|
it('should work with query result', () => {
|
||||||
|
const validQueryResult = peopleQueryResult.people;
|
||||||
|
|
||||||
|
const isValidQueryResult = isObjectRecordConnection(
|
||||||
|
'person',
|
||||||
|
validQueryResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isValidQueryResult).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail with invalid result', () => {
|
||||||
|
const invalidResult = { test: 123 };
|
||||||
|
|
||||||
|
const isValidQueryResult = isObjectRecordConnection(
|
||||||
|
'person',
|
||||||
|
invalidResult,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isValidQueryResult).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_MUTATION_BATCH_SIZE = 30;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_QUERY_PAGE_SIZE = 30;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const DELETE_MAX_COUNT = 10000;
|
||||||
@ -6,6 +6,7 @@ export type RecordGqlConnection = {
|
|||||||
__typename?: string;
|
__typename?: string;
|
||||||
edges: RecordGqlEdge[];
|
edges: RecordGqlEdge[];
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
|
__typename?: string;
|
||||||
hasNextPage?: boolean;
|
hasNextPage?: boolean;
|
||||||
hasPreviousPage?: boolean;
|
hasPreviousPage?: boolean;
|
||||||
startCursor?: Nullable<string>;
|
startCursor?: Nullable<string>;
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
import { peopleQueryResult } from '~/testing/mock-data/people';
|
||||||
|
|
||||||
|
|
||||||
|
export const query = gql`
|
||||||
|
query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) {
|
||||||
|
people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const mockPageSize = 2;
|
||||||
|
|
||||||
|
export const peopleMockWithIdsOnly: RecordGqlConnection = { ...peopleQueryResult.people,edges: peopleQueryResult.people.edges.map((edge) => ({ ...edge, node: { __typename: 'Person', id: edge.node.id } })) };
|
||||||
|
|
||||||
|
export const firstRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize].cursor;
|
||||||
|
export const secondRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor;
|
||||||
|
export const thirdRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor;
|
||||||
|
|
||||||
|
export const variablesFirstRequest = {
|
||||||
|
filter: undefined,
|
||||||
|
limit: undefined,
|
||||||
|
orderBy: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
export const variablesSecondRequest = {
|
||||||
|
filter: undefined,
|
||||||
|
limit: undefined,
|
||||||
|
orderBy: undefined,
|
||||||
|
lastCursor: firstRequestLastCursor
|
||||||
|
};
|
||||||
|
|
||||||
|
export const variablesThirdRequest = {
|
||||||
|
filter: undefined,
|
||||||
|
limit: undefined,
|
||||||
|
orderBy: undefined,
|
||||||
|
lastCursor: secondRequestLastCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginateRequestResponse = (response: RecordGqlConnection, start: number, end: number, hasNextPage: boolean, totalCount: number) => {
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
edges: [
|
||||||
|
...response.edges.slice(start, end)
|
||||||
|
],
|
||||||
|
pageInfo: {
|
||||||
|
...response.pageInfo,
|
||||||
|
startCursor: response.edges[start].cursor,
|
||||||
|
endCursor: response.edges[end].cursor,
|
||||||
|
hasNextPage,
|
||||||
|
} satisfies RecordGqlConnection['pageInfo'],
|
||||||
|
totalCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const responseFirstRequest = {
|
||||||
|
people: paginateRequestResponse(peopleMockWithIdsOnly, 0, mockPageSize, true, 6),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const responseSecondRequest = {
|
||||||
|
people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize, mockPageSize * 2, true, 6),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const responseThirdRequest = {
|
||||||
|
people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize * 2, mockPageSize * 3, false, 6),
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -23,7 +23,7 @@ const mocks = [
|
|||||||
},
|
},
|
||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
data: {
|
data: {
|
||||||
deletePeople: responseData,
|
deletePeople: [responseData],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@ describe('useDeleteManyRecords', () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
const res = await result.current.deleteManyRecords(people);
|
const res = await result.current.deleteManyRecords(people);
|
||||||
expect(res).toBeDefined();
|
expect(res).toBeDefined();
|
||||||
expect(res).toHaveProperty('id');
|
expect(res[0]).toHaveProperty('id');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks[0].result).toHaveBeenCalled();
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
import { RecoilRoot, useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
|
import {
|
||||||
|
mockPageSize,
|
||||||
|
peopleMockWithIdsOnly,
|
||||||
|
query,
|
||||||
|
responseFirstRequest,
|
||||||
|
responseSecondRequest,
|
||||||
|
responseThirdRequest,
|
||||||
|
variablesFirstRequest,
|
||||||
|
variablesSecondRequest,
|
||||||
|
variablesThirdRequest,
|
||||||
|
} from '@/object-record/hooks/__mocks__/useFetchAllRecordIds';
|
||||||
|
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
||||||
|
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
|
||||||
|
|
||||||
|
const mocks = [
|
||||||
|
{
|
||||||
|
delay: 100,
|
||||||
|
request: {
|
||||||
|
query,
|
||||||
|
variables: variablesFirstRequest,
|
||||||
|
},
|
||||||
|
result: jest.fn(() => ({
|
||||||
|
data: responseFirstRequest,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 100,
|
||||||
|
request: {
|
||||||
|
query,
|
||||||
|
variables: variablesSecondRequest,
|
||||||
|
},
|
||||||
|
result: jest.fn(() => ({
|
||||||
|
data: responseSecondRequest,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 100,
|
||||||
|
request: {
|
||||||
|
query,
|
||||||
|
variables: variablesThirdRequest,
|
||||||
|
},
|
||||||
|
result: jest.fn(() => ({
|
||||||
|
data: responseThirdRequest,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('useFetchAllRecordIds', () => {
|
||||||
|
it('fetches all record ids with fetch more synchronous loop', async () => {
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<SnackBarManagerScopeInternalContext.Provider
|
||||||
|
value={{
|
||||||
|
scopeId: 'snack-bar-manager',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MockedProvider mocks={mocks} addTypename={false}>
|
||||||
|
{children}
|
||||||
|
</MockedProvider>
|
||||||
|
</SnackBarManagerScopeInternalContext.Provider>
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => {
|
||||||
|
const [, setObjectMetadataItems] = useRecoilState(
|
||||||
|
objectMetadataItemsState,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setObjectMetadataItems(getObjectMetadataItemsMock());
|
||||||
|
}, [setObjectMetadataItems]);
|
||||||
|
|
||||||
|
return useFetchAllRecordIds({
|
||||||
|
objectNameSingular: 'person',
|
||||||
|
pageSize: mockPageSize,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchAllRecordIds } = result.current;
|
||||||
|
|
||||||
|
let recordIds: string[] = [];
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
recordIds = await fetchAllRecordIds();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks[0].result).toHaveBeenCalled();
|
||||||
|
expect(mocks[1].result).toHaveBeenCalled();
|
||||||
|
expect(mocks[2].result).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(recordIds).toEqual(
|
||||||
|
peopleMockWithIdsOnly.edges.map((edge) => edge.node.id).slice(0, 6),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||||
|
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||||
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
|
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
|
||||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
@ -16,6 +18,7 @@ type useDeleteOneRecordProps = {
|
|||||||
|
|
||||||
type DeleteManyRecordsOptions = {
|
type DeleteManyRecordsOptions = {
|
||||||
skipOptimisticEffect?: boolean;
|
skipOptimisticEffect?: boolean;
|
||||||
|
delayInMsBetweenRequests?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteManyRecords = ({
|
export const useDeleteManyRecords = ({
|
||||||
@ -45,40 +48,62 @@ export const useDeleteManyRecords = ({
|
|||||||
idsToDelete: string[],
|
idsToDelete: string[],
|
||||||
options?: DeleteManyRecordsOptions,
|
options?: DeleteManyRecordsOptions,
|
||||||
) => {
|
) => {
|
||||||
const deletedRecords = await apolloClient.mutate({
|
const numberOfBatches = Math.ceil(
|
||||||
mutation: deleteManyRecordsMutation,
|
idsToDelete.length / DEFAULT_MUTATION_BATCH_SIZE,
|
||||||
variables: {
|
);
|
||||||
filter: { id: { in: idsToDelete } },
|
|
||||||
},
|
|
||||||
optimisticResponse: options?.skipOptimisticEffect
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
|
||||||
__typename: capitalize(objectNameSingular),
|
|
||||||
id: idToDelete,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
update: options?.skipOptimisticEffect
|
|
||||||
? undefined
|
|
||||||
: (cache, { data }) => {
|
|
||||||
const records = data?.[mutationResponseField];
|
|
||||||
|
|
||||||
if (!records?.length) return;
|
const deletedRecords = [];
|
||||||
|
|
||||||
const cachedRecords = records
|
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||||
.map((record) => getRecordFromCache(record.id, cache))
|
const batchIds = idsToDelete.slice(
|
||||||
.filter(isDefined);
|
batchIndex * DEFAULT_MUTATION_BATCH_SIZE,
|
||||||
|
(batchIndex + 1) * DEFAULT_MUTATION_BATCH_SIZE,
|
||||||
|
);
|
||||||
|
|
||||||
triggerDeleteRecordsOptimisticEffect({
|
const deletedRecordsResponse = await apolloClient.mutate({
|
||||||
cache,
|
mutation: deleteManyRecordsMutation,
|
||||||
objectMetadataItem,
|
variables: {
|
||||||
recordsToDelete: cachedRecords,
|
filter: { id: { in: batchIds } },
|
||||||
objectMetadataItems,
|
},
|
||||||
});
|
optimisticResponse: options?.skipOptimisticEffect
|
||||||
},
|
? undefined
|
||||||
});
|
: {
|
||||||
|
[mutationResponseField]: batchIds.map((idToDelete) => ({
|
||||||
|
__typename: capitalize(objectNameSingular),
|
||||||
|
id: idToDelete,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
update: options?.skipOptimisticEffect
|
||||||
|
? undefined
|
||||||
|
: (cache, { data }) => {
|
||||||
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
return deletedRecords.data?.[mutationResponseField] ?? null;
|
if (!records?.length) return;
|
||||||
|
|
||||||
|
const cachedRecords = records
|
||||||
|
.map((record) => getRecordFromCache(record.id, cache))
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordsToDelete: cachedRecords,
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedRecordsForThisBatch =
|
||||||
|
deletedRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||||
|
|
||||||
|
deletedRecords.push(...deletedRecordsForThisBatch);
|
||||||
|
|
||||||
|
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||||
|
await sleep(options.delayInMsBetweenRequests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedRecords;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { deleteManyRecords };
|
return { deleteManyRecords };
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||||
|
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||||
|
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type UseLazyFetchAllRecordIdsParams<T> = Omit<
|
||||||
|
UseFindManyRecordsParams<T>,
|
||||||
|
'skip'
|
||||||
|
> & { pageSize?: number };
|
||||||
|
|
||||||
|
export const useFetchAllRecordIds = <T>({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
pageSize = DEFAULT_QUERY_PAGE_SIZE,
|
||||||
|
}: UseLazyFetchAllRecordIdsParams<T>) => {
|
||||||
|
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
recordGqlFields: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAllRecordIds = useCallback(async () => {
|
||||||
|
if (!isDefined(findManyRecords)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const findManyRecordsDataResult = await findManyRecords();
|
||||||
|
|
||||||
|
const firstQueryResult =
|
||||||
|
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
||||||
|
|
||||||
|
const totalCount = firstQueryResult?.totalCount ?? 1;
|
||||||
|
|
||||||
|
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
||||||
|
|
||||||
|
const recordIdSet = new Set(
|
||||||
|
firstQueryResult?.edges?.map((edge) => edge.node.id) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const remainingCount = totalCount - recordsCount;
|
||||||
|
|
||||||
|
const remainingPages = Math.ceil(remainingCount / pageSize);
|
||||||
|
|
||||||
|
let lastCursor = firstQueryResult?.pageInfo.endCursor ?? '';
|
||||||
|
|
||||||
|
for (let i = 0; i < remainingPages; i++) {
|
||||||
|
const rawResult = await fetchMore?.({
|
||||||
|
variables: {
|
||||||
|
lastCursor: lastCursor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
|
||||||
|
|
||||||
|
for (const edge of fetchMoreResult.edges) {
|
||||||
|
recordIdSet.add(edge.node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCursor = fetchMoreResult.pageInfo.endCursor ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordIds = Array.from(recordIdSet);
|
||||||
|
|
||||||
|
return recordIds;
|
||||||
|
}, [fetchMore, findManyRecords, objectMetadataItem.namePlural, pageSize]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchAllRecordIds,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
import {
|
||||||
|
ApolloError,
|
||||||
|
ApolloQueryResult,
|
||||||
|
FetchMoreQueryOptions,
|
||||||
|
OperationVariables,
|
||||||
|
WatchQueryFetchPolicy,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
|
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
|
||||||
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
|
import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
|
||||||
|
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||||
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
|
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
|
||||||
|
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
||||||
|
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
import { cursorFamilyState } from '../states/cursorFamilyState';
|
||||||
|
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
|
||||||
|
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
|
||||||
|
|
||||||
|
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
|
||||||
|
RecordGqlOperationVariables & {
|
||||||
|
onCompleted?: OnFindManyRecordsCompleted<T>;
|
||||||
|
skip?: boolean;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
fetchPolicy?: WatchQueryFetchPolicy;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseFindManyRecordsStateParams<
|
||||||
|
T,
|
||||||
|
TData = RecordGqlOperationFindManyResult,
|
||||||
|
> = Omit<
|
||||||
|
UseFindManyRecordsParams<T>,
|
||||||
|
'skip' | 'recordGqlFields' | 'fetchPolicy'
|
||||||
|
> & {
|
||||||
|
data: RecordGqlOperationFindManyResult | undefined;
|
||||||
|
error: ApolloError | undefined;
|
||||||
|
fetchMore<
|
||||||
|
TFetchData = TData,
|
||||||
|
TFetchVars extends OperationVariables = OperationVariables,
|
||||||
|
>(
|
||||||
|
fetchMoreOptions: FetchMoreQueryOptions<TFetchVars, TFetchData> & {
|
||||||
|
updateQuery?: (
|
||||||
|
previousQueryResult: TData,
|
||||||
|
options: {
|
||||||
|
fetchMoreResult: TFetchData;
|
||||||
|
variables: TFetchVars;
|
||||||
|
},
|
||||||
|
) => TData;
|
||||||
|
},
|
||||||
|
): Promise<ApolloQueryResult<TFetchData>>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFetchMoreRecordsWithPagination = <
|
||||||
|
T extends ObjectRecord = ObjectRecord,
|
||||||
|
>({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
fetchMore,
|
||||||
|
objectMetadataItem,
|
||||||
|
onCompleted,
|
||||||
|
}: UseFindManyRecordsStateParams<T>) => {
|
||||||
|
const queryIdentifier = getQueryIdentifier({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [hasNextPage] = useRecoilState(hasNextPageFamilyState(queryIdentifier));
|
||||||
|
|
||||||
|
const setIsFetchingMoreObjects = useSetRecoilState(
|
||||||
|
isFetchingMoreRecordsFamilyState(queryIdentifier),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: put this into a util inspired from https://github.com/apollographql/apollo-client/blob/master/src/utilities/policies/pagination.ts
|
||||||
|
// This function is equivalent to merge function + read function in field policy
|
||||||
|
const fetchMoreRecords = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
async () => {
|
||||||
|
const hasNextPageLocal = snapshot
|
||||||
|
.getLoadable(hasNextPageFamilyState(queryIdentifier))
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const lastCursorLocal = snapshot
|
||||||
|
.getLoadable(cursorFamilyState(queryIdentifier))
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
// Remote objects does not support hasNextPage. We cannot rely on it to fetch more records.
|
||||||
|
if (
|
||||||
|
hasNextPageLocal ||
|
||||||
|
(!isAggregationEnabled(objectMetadataItem) && !error)
|
||||||
|
) {
|
||||||
|
setIsFetchingMoreObjects(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: fetchMoreDataResult } = await fetchMore({
|
||||||
|
variables: {
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
lastCursor: isNonEmptyString(lastCursorLocal)
|
||||||
|
? lastCursorLocal
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
|
const previousEdges =
|
||||||
|
prev?.[objectMetadataItem.namePlural]?.edges;
|
||||||
|
const nextEdges =
|
||||||
|
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
|
||||||
|
|
||||||
|
let newEdges: RecordGqlEdge[] = previousEdges ?? [];
|
||||||
|
|
||||||
|
if (isNonEmptyArray(nextEdges)) {
|
||||||
|
newEdges = filterUniqueRecordEdgesByCursor([
|
||||||
|
...newEdges,
|
||||||
|
...(fetchMoreResult?.[objectMetadataItem.namePlural]
|
||||||
|
?.edges ?? []),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageInfo =
|
||||||
|
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
|
||||||
|
|
||||||
|
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
||||||
|
set(
|
||||||
|
cursorFamilyState(queryIdentifier),
|
||||||
|
pageInfo.endCursor ?? '',
|
||||||
|
);
|
||||||
|
set(
|
||||||
|
hasNextPageFamilyState(queryIdentifier),
|
||||||
|
pageInfo.hasNextPage ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = getRecordsFromRecordConnection({
|
||||||
|
recordConnection: {
|
||||||
|
edges: newEdges,
|
||||||
|
pageInfo,
|
||||||
|
},
|
||||||
|
}) as T[];
|
||||||
|
|
||||||
|
onCompleted?.(records, {
|
||||||
|
pageInfo,
|
||||||
|
totalCount:
|
||||||
|
fetchMoreResult?.[objectMetadataItem.namePlural]
|
||||||
|
?.totalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign({}, prev, {
|
||||||
|
[objectMetadataItem.namePlural]: {
|
||||||
|
__typename: `${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}Connection`,
|
||||||
|
edges: newEdges,
|
||||||
|
pageInfo:
|
||||||
|
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
|
||||||
|
totalCount:
|
||||||
|
fetchMoreResult?.[objectMetadataItem.namePlural]
|
||||||
|
.totalCount,
|
||||||
|
},
|
||||||
|
} as RecordGqlOperationFindManyResult);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: fetchMoreDataResult?.[objectMetadataItem.namePlural],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
handleFindManyRecordsError(error as ApolloError);
|
||||||
|
} finally {
|
||||||
|
setIsFetchingMoreObjects(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
objectMetadataItem,
|
||||||
|
error,
|
||||||
|
setIsFetchingMoreObjects,
|
||||||
|
fetchMore,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
data,
|
||||||
|
onCompleted,
|
||||||
|
handleFindManyRecordsError,
|
||||||
|
queryIdentifier,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount;
|
||||||
|
|
||||||
|
const records = useMemo(
|
||||||
|
() =>
|
||||||
|
data?.[objectMetadataItem.namePlural]
|
||||||
|
? getRecordsFromRecordConnection<T>({
|
||||||
|
recordConnection: data?.[objectMetadataItem.namePlural],
|
||||||
|
})
|
||||||
|
: ([] as T[]),
|
||||||
|
|
||||||
|
[data, objectMetadataItem.namePlural],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchMoreRecords,
|
||||||
|
totalCount,
|
||||||
|
records,
|
||||||
|
hasNextPage,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,85 +1,66 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
||||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
|
|
||||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
|
||||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
|
||||||
import { RecordGqlEdge } from '@/object-record/graphql/types/RecordGqlEdge';
|
|
||||||
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||||
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
|
import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||||
|
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
|
||||||
|
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
import { logError } from '~/utils/logError';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
|
||||||
|
|
||||||
import { cursorFamilyState } from '../states/cursorFamilyState';
|
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
|
||||||
import { hasNextPageFamilyState } from '../states/hasNextPageFamilyState';
|
RecordGqlOperationVariables & {
|
||||||
import { isFetchingMoreRecordsFamilyState } from '../states/isFetchingMoreRecordsFamilyState';
|
onError?: (error?: Error) => void;
|
||||||
|
onCompleted?: OnFindManyRecordsCompleted<T>;
|
||||||
|
skip?: boolean;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
fetchPolicy?: WatchQueryFetchPolicy;
|
||||||
|
};
|
||||||
|
|
||||||
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter,
|
filter,
|
||||||
orderBy,
|
orderBy,
|
||||||
limit,
|
limit,
|
||||||
onCompleted,
|
|
||||||
onError,
|
|
||||||
skip,
|
skip,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
}: ObjectMetadataItemIdentifier &
|
onError,
|
||||||
RecordGqlOperationVariables & {
|
onCompleted,
|
||||||
onCompleted?: (
|
}: UseFindManyRecordsParams<T>) => {
|
||||||
records: T[],
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
options?: {
|
|
||||||
pageInfo?: RecordGqlConnection['pageInfo'];
|
|
||||||
totalCount?: number;
|
|
||||||
},
|
|
||||||
) => void;
|
|
||||||
onError?: (error?: Error) => void;
|
|
||||||
skip?: boolean;
|
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
|
||||||
fetchPolicy?: WatchQueryFetchPolicy;
|
|
||||||
}) => {
|
|
||||||
const findManyQueryStateIdentifier =
|
|
||||||
objectNameSingular +
|
|
||||||
JSON.stringify(filter) +
|
|
||||||
JSON.stringify(orderBy) +
|
|
||||||
limit;
|
|
||||||
|
|
||||||
const [lastCursor, setLastCursor] = useRecoilState(
|
|
||||||
cursorFamilyState(findManyQueryStateIdentifier),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [hasNextPage, setHasNextPage] = useRecoilState(
|
|
||||||
hasNextPageFamilyState(findManyQueryStateIdentifier),
|
|
||||||
);
|
|
||||||
|
|
||||||
const setIsFetchingMoreObjects = useSetRecoilState(
|
|
||||||
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
objectMetadataItem,
|
||||||
|
handleError: onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryIdentifier = getQueryIdentifier({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({
|
||||||
|
objectMetadataItem,
|
||||||
|
queryIdentifier,
|
||||||
|
onCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error, fetchMore } =
|
const { data, loading, error, fetchMore } =
|
||||||
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
|
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
|
||||||
@ -90,147 +71,21 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
orderBy,
|
orderBy,
|
||||||
},
|
},
|
||||||
fetchPolicy: fetchPolicy,
|
fetchPolicy: fetchPolicy,
|
||||||
onCompleted: (data) => {
|
onCompleted: handleFindManyRecordsCompleted,
|
||||||
if (!isDefined(data)) {
|
onError: handleFindManyRecordsError,
|
||||||
onCompleted?.([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
|
|
||||||
|
|
||||||
const records = getRecordsFromRecordConnection({
|
|
||||||
recordConnection: data?.[objectMetadataItem.namePlural],
|
|
||||||
}) as T[];
|
|
||||||
|
|
||||||
onCompleted?.(records, {
|
|
||||||
pageInfo,
|
|
||||||
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
|
||||||
setLastCursor(pageInfo.endCursor ?? '');
|
|
||||||
setHasNextPage(pageInfo.hasNextPage ?? false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
logError(
|
|
||||||
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
enqueueSnackBar(
|
|
||||||
`Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
|
||||||
{
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
onError?.(error);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchMoreRecords = useCallback(async () => {
|
const { fetchMoreRecords, totalCount, records, hasNextPage } =
|
||||||
// Remote objects does not support hasNextPage. We cannot rely on it to fetch more records.
|
useFetchMoreRecordsWithPagination<T>({
|
||||||
if (hasNextPage || (!isAggregationEnabled(objectMetadataItem) && !error)) {
|
objectNameSingular,
|
||||||
setIsFetchingMoreObjects(true);
|
filter,
|
||||||
|
orderBy,
|
||||||
try {
|
limit,
|
||||||
await fetchMore({
|
fetchMore,
|
||||||
variables: {
|
data,
|
||||||
filter,
|
error,
|
||||||
orderBy,
|
objectMetadataItem,
|
||||||
lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined,
|
});
|
||||||
},
|
|
||||||
updateQuery: (prev, { fetchMoreResult }) => {
|
|
||||||
const previousEdges = prev?.[objectMetadataItem.namePlural]?.edges;
|
|
||||||
const nextEdges =
|
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.edges;
|
|
||||||
|
|
||||||
let newEdges: RecordGqlEdge[] = previousEdges ?? [];
|
|
||||||
|
|
||||||
if (isNonEmptyArray(nextEdges)) {
|
|
||||||
newEdges = filterUniqueRecordEdgesByCursor([
|
|
||||||
...newEdges,
|
|
||||||
...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ??
|
|
||||||
[]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageInfo =
|
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.pageInfo;
|
|
||||||
|
|
||||||
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
|
||||||
setLastCursor(pageInfo.endCursor ?? '');
|
|
||||||
setHasNextPage(pageInfo.hasNextPage ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const records = getRecordsFromRecordConnection({
|
|
||||||
recordConnection: {
|
|
||||||
edges: newEdges,
|
|
||||||
pageInfo,
|
|
||||||
},
|
|
||||||
}) as T[];
|
|
||||||
|
|
||||||
onCompleted?.(records, {
|
|
||||||
pageInfo,
|
|
||||||
totalCount:
|
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural]?.totalCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign({}, prev, {
|
|
||||||
[objectMetadataItem.namePlural]: {
|
|
||||||
__typename: `${capitalize(
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
)}Connection`,
|
|
||||||
edges: newEdges,
|
|
||||||
pageInfo:
|
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural].pageInfo,
|
|
||||||
totalCount:
|
|
||||||
fetchMoreResult?.[objectMetadataItem.namePlural].totalCount,
|
|
||||||
},
|
|
||||||
} as RecordGqlOperationFindManyResult);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logError(
|
|
||||||
`fetchMoreObjects for "${objectMetadataItem.namePlural}" error : ` +
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
enqueueSnackBar(
|
|
||||||
`Error during fetchMoreObjects for "${objectMetadataItem.namePlural}", ${error}`,
|
|
||||||
{
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsFetchingMoreObjects(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
hasNextPage,
|
|
||||||
objectMetadataItem,
|
|
||||||
error,
|
|
||||||
setIsFetchingMoreObjects,
|
|
||||||
fetchMore,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
lastCursor,
|
|
||||||
data,
|
|
||||||
onCompleted,
|
|
||||||
setLastCursor,
|
|
||||||
setHasNextPage,
|
|
||||||
enqueueSnackBar,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const totalCount = data?.[objectMetadataItem.namePlural]?.totalCount;
|
|
||||||
|
|
||||||
const records = useMemo(
|
|
||||||
() =>
|
|
||||||
data?.[objectMetadataItem.namePlural]
|
|
||||||
? getRecordsFromRecordConnection<T>({
|
|
||||||
recordConnection: data?.[objectMetadataItem.namePlural],
|
|
||||||
})
|
|
||||||
: ([] as T[]),
|
|
||||||
|
|
||||||
[data, objectMetadataItem.namePlural],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -239,7 +94,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
queryStateIdentifier: findManyQueryStateIdentifier,
|
queryStateIdentifier: queryIdentifier,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||||
|
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||||
|
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
|
||||||
|
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
|
||||||
|
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const useHandleFindManyRecordsCompleted = <T>({
|
||||||
|
queryIdentifier,
|
||||||
|
onCompleted,
|
||||||
|
objectMetadataItem,
|
||||||
|
}: {
|
||||||
|
queryIdentifier: string;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
onCompleted?: OnFindManyRecordsCompleted<T>;
|
||||||
|
}) => {
|
||||||
|
const [, setLastCursor] = useRecoilState(cursorFamilyState(queryIdentifier));
|
||||||
|
|
||||||
|
const [, setHasNextPage] = useRecoilState(
|
||||||
|
hasNextPageFamilyState(queryIdentifier),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFindManyRecordsCompleted = (
|
||||||
|
data: RecordGqlOperationFindManyResult,
|
||||||
|
) => {
|
||||||
|
if (!isDefined(data)) {
|
||||||
|
onCompleted?.([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageInfo = data?.[objectMetadataItem.namePlural]?.pageInfo;
|
||||||
|
|
||||||
|
const records = getRecordsFromRecordConnection({
|
||||||
|
recordConnection: data?.[objectMetadataItem.namePlural],
|
||||||
|
}) as T[];
|
||||||
|
|
||||||
|
onCompleted?.(records, {
|
||||||
|
pageInfo,
|
||||||
|
totalCount: data?.[objectMetadataItem.namePlural]?.totalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDefined(data?.[objectMetadataItem.namePlural])) {
|
||||||
|
setLastCursor(pageInfo.endCursor ?? '');
|
||||||
|
setHasNextPage(pageInfo.hasNextPage ?? false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleFindManyRecordsCompleted,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { ApolloError } from '@apollo/client';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { logError } from '~/utils/logError';
|
||||||
|
|
||||||
|
export const useHandleFindManyRecordsError = ({
|
||||||
|
handleError,
|
||||||
|
objectMetadataItem,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
handleError?: (error?: Error) => void;
|
||||||
|
}) => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const handleFindManyRecordsError = (error: ApolloError) => {
|
||||||
|
logError(
|
||||||
|
`useFindManyRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
enqueueSnackBar(
|
||||||
|
`Error during useFindManyRecords for "${objectMetadataItem.namePlural}", ${error.message}`,
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
handleError?.(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleFindManyRecordsError,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult';
|
||||||
|
import { useFetchMoreRecordsWithPagination } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||||
|
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||||
|
import { useHandleFindManyRecordsCompleted } from '@/object-record/hooks/useHandleFindManyRecordsCompleted';
|
||||||
|
import { useHandleFindManyRecordsError } from '@/object-record/hooks/useHandleFindManyRecordsError';
|
||||||
|
import { cursorFamilyState } from '@/object-record/states/cursorFamilyState';
|
||||||
|
import { hasNextPageFamilyState } from '@/object-record/states/hasNextPageFamilyState';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { getQueryIdentifier } from '@/object-record/utils/getQueryIdentifier';
|
||||||
|
|
||||||
|
type UseLazyFindManyRecordsParams<T> = Omit<
|
||||||
|
UseFindManyRecordsParams<T>,
|
||||||
|
'skip'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useLazyFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
recordGqlFields,
|
||||||
|
fetchPolicy,
|
||||||
|
onCompleted,
|
||||||
|
onError,
|
||||||
|
}: UseLazyFindManyRecordsParams<T>) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { findManyRecordsQuery } = useFindManyRecordsQuery({
|
||||||
|
objectNameSingular,
|
||||||
|
recordGqlFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleFindManyRecordsError } = useHandleFindManyRecordsError({
|
||||||
|
objectMetadataItem,
|
||||||
|
handleError: onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryIdentifier = getQueryIdentifier({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleFindManyRecordsCompleted } = useHandleFindManyRecordsCompleted({
|
||||||
|
objectMetadataItem,
|
||||||
|
queryIdentifier,
|
||||||
|
onCompleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [findManyRecords, { data, loading, error, fetchMore }] =
|
||||||
|
useLazyQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
|
||||||
|
variables: {
|
||||||
|
filter,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
},
|
||||||
|
fetchPolicy: fetchPolicy,
|
||||||
|
onCompleted: handleFindManyRecordsCompleted,
|
||||||
|
onError: handleFindManyRecordsError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fetchMoreRecords, totalCount, records } =
|
||||||
|
useFetchMoreRecordsWithPagination<T>({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
onCompleted,
|
||||||
|
fetchMore,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
const findManyRecordsLazy = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
async () => {
|
||||||
|
const result = await findManyRecords();
|
||||||
|
|
||||||
|
const hasNextPage =
|
||||||
|
result?.data?.[objectMetadataItem.namePlural]?.pageInfo.hasNextPage ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
const lastCursor =
|
||||||
|
result?.data?.[objectMetadataItem.namePlural]?.pageInfo.endCursor ??
|
||||||
|
'';
|
||||||
|
|
||||||
|
set(hasNextPageFamilyState(queryIdentifier), hasNextPage);
|
||||||
|
set(cursorFamilyState(queryIdentifier), lastCursor);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[queryIdentifier, findManyRecords, objectMetadataItem],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectMetadataItem,
|
||||||
|
records,
|
||||||
|
totalCount,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchMore,
|
||||||
|
fetchMoreRecordsWithPagination: fetchMoreRecords,
|
||||||
|
queryStateIdentifier: queryIdentifier,
|
||||||
|
findManyRecords: findManyRecordsLazy,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
IconClick,
|
IconClick,
|
||||||
IconFileExport,
|
IconFileExport,
|
||||||
@ -11,11 +11,11 @@ import {
|
|||||||
IconTrash,
|
IconTrash,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||||
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
|
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
|
||||||
|
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
|
||||||
import {
|
import {
|
||||||
displayedExportProgress,
|
displayedExportProgress,
|
||||||
useExportTableData,
|
useExportTableData,
|
||||||
@ -32,12 +32,14 @@ type useRecordActionBarProps = {
|
|||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
selectedRecordIds: string[];
|
selectedRecordIds: string[];
|
||||||
callback?: () => void;
|
callback?: () => void;
|
||||||
|
totalNumberOfRecordsSelected?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRecordActionBar = ({
|
export const useRecordActionBar = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
selectedRecordIds,
|
selectedRecordIds,
|
||||||
callback,
|
callback,
|
||||||
|
totalNumberOfRecordsSelected,
|
||||||
}: useRecordActionBarProps) => {
|
}: useRecordActionBarProps) => {
|
||||||
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
|
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
|
||||||
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
|
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
|
||||||
@ -46,17 +48,10 @@ export const useRecordActionBar = ({
|
|||||||
|
|
||||||
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
||||||
|
|
||||||
const { deleteManyRecords } = useDeleteManyRecords({
|
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({
|
const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiConfig = useRecoilValue(apiConfigState);
|
|
||||||
const maxRecords = apiConfig?.mutationMaximumAffectedRecords;
|
|
||||||
|
|
||||||
const handleFavoriteButtonClick = useRecoilCallback(
|
const handleFavoriteButtonClick = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
() => {
|
() => {
|
||||||
@ -92,24 +87,17 @@ export const useRecordActionBar = ({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteClick = useCallback(async () => {
|
const baseTableDataParams = {
|
||||||
callback?.();
|
delayMs: 100,
|
||||||
selectedRecordIds.forEach((recordId) => {
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
const foundFavorite = favorites?.find(
|
recordIndexId: objectMetadataItem.namePlural,
|
||||||
(favorite) => favorite.recordId === recordId,
|
};
|
||||||
);
|
|
||||||
if (foundFavorite !== undefined) {
|
const { deleteTableData } = useDeleteTableData(baseTableDataParams);
|
||||||
deleteFavorite(foundFavorite.id);
|
|
||||||
}
|
const handleDeleteClick = useCallback(() => {
|
||||||
});
|
deleteTableData();
|
||||||
await deleteManyRecords(selectedRecordIds);
|
}, [deleteTableData]);
|
||||||
}, [
|
|
||||||
callback,
|
|
||||||
deleteManyRecords,
|
|
||||||
selectedRecordIds,
|
|
||||||
favorites,
|
|
||||||
deleteFavorite,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleExecuteQuickActionOnClick = useCallback(async () => {
|
const handleExecuteQuickActionOnClick = useCallback(async () => {
|
||||||
callback?.();
|
callback?.();
|
||||||
@ -121,62 +109,63 @@ export const useRecordActionBar = ({
|
|||||||
}, [callback, executeQuickActionOnOneRecord, selectedRecordIds]);
|
}, [callback, executeQuickActionOnOneRecord, selectedRecordIds]);
|
||||||
|
|
||||||
const { progress, download } = useExportTableData({
|
const { progress, download } = useExportTableData({
|
||||||
delayMs: 100,
|
...baseTableDataParams,
|
||||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
|
||||||
recordIndexId: objectMetadataItem.namePlural,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isRemoteObject = objectMetadataItem.isRemote;
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
const baseActions: ContextMenuEntry[] = useMemo(
|
const numberOfSelectedRecords =
|
||||||
() => [
|
totalNumberOfRecordsSelected ?? selectedRecordIds.length;
|
||||||
{
|
const canDelete =
|
||||||
label: displayedExportProgress(progress),
|
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
|
||||||
Icon: IconFileExport,
|
|
||||||
accent: 'default',
|
|
||||||
onClick: () => download(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[download, progress],
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletionActions: ContextMenuEntry[] = useMemo(
|
const menuActions: ContextMenuEntry[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
maxRecords !== undefined && selectedRecordIds.length <= maxRecords
|
[
|
||||||
? [
|
{
|
||||||
{
|
label: displayedExportProgress(progress),
|
||||||
|
Icon: IconFileExport,
|
||||||
|
accent: 'default',
|
||||||
|
onClick: () => download(),
|
||||||
|
} satisfies ContextMenuEntry,
|
||||||
|
canDelete
|
||||||
|
? ({
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
Icon: IconTrash,
|
Icon: IconTrash,
|
||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
onClick: () => setIsDeleteRecordsModalOpen(true),
|
onClick: () => {
|
||||||
|
setIsDeleteRecordsModalOpen(true);
|
||||||
|
handleDeleteClick();
|
||||||
|
},
|
||||||
ConfirmationModal: (
|
ConfirmationModal: (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isDeleteRecordsModalOpen}
|
isOpen={isDeleteRecordsModalOpen}
|
||||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||||
title={`Delete ${selectedRecordIds.length} ${
|
title={`Delete ${numberOfSelectedRecords} ${
|
||||||
selectedRecordIds.length === 1 ? `record` : 'records'
|
numberOfSelectedRecords === 1 ? `record` : 'records'
|
||||||
}`}
|
}`}
|
||||||
subtitle={`This action cannot be undone. This will permanently delete ${
|
subtitle={`This action cannot be undone. This will permanently delete ${
|
||||||
selectedRecordIds.length === 1
|
numberOfSelectedRecords === 1
|
||||||
? 'this record'
|
? 'this record'
|
||||||
: 'these records'
|
: 'these records'
|
||||||
}`}
|
}`}
|
||||||
onConfirmClick={() => handleDeleteClick()}
|
onConfirmClick={() => handleDeleteClick()}
|
||||||
deleteButtonText={`Delete ${
|
deleteButtonText={`Delete ${
|
||||||
selectedRecordIds.length > 1 ? 'Records' : 'Record'
|
numberOfSelectedRecords > 1 ? 'Records' : 'Record'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
} satisfies ContextMenuEntry)
|
||||||
]
|
: undefined,
|
||||||
: [],
|
].filter(isDefined),
|
||||||
[
|
[
|
||||||
|
download,
|
||||||
|
progress,
|
||||||
|
canDelete,
|
||||||
handleDeleteClick,
|
handleDeleteClick,
|
||||||
selectedRecordIds,
|
|
||||||
isDeleteRecordsModalOpen,
|
isDeleteRecordsModalOpen,
|
||||||
setIsDeleteRecordsModalOpen,
|
numberOfSelectedRecords,
|
||||||
maxRecords,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -193,8 +182,7 @@ export const useRecordActionBar = ({
|
|||||||
return {
|
return {
|
||||||
setContextMenuEntries: useCallback(() => {
|
setContextMenuEntries: useCallback(() => {
|
||||||
setContextMenuEntries([
|
setContextMenuEntries([
|
||||||
...(isRemoteObject ? [] : deletionActions),
|
...menuActions,
|
||||||
...baseActions,
|
|
||||||
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
|
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -215,8 +203,7 @@ export const useRecordActionBar = ({
|
|||||||
: []),
|
: []),
|
||||||
]);
|
]);
|
||||||
}, [
|
}, [
|
||||||
baseActions,
|
menuActions,
|
||||||
deletionActions,
|
|
||||||
handleFavoriteButtonClick,
|
handleFavoriteButtonClick,
|
||||||
hasOnlyOneRecordSelected,
|
hasOnlyOneRecordSelected,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
@ -245,15 +232,12 @@ export const useRecordActionBar = ({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(isRemoteObject ? [] : deletionActions),
|
...menuActions,
|
||||||
...baseActions,
|
|
||||||
]);
|
]);
|
||||||
}, [
|
}, [
|
||||||
baseActions,
|
menuActions,
|
||||||
dataExecuteQuickActionOnmentEnabled,
|
dataExecuteQuickActionOnmentEnabled,
|
||||||
deletionActions,
|
|
||||||
handleExecuteQuickActionOnClick,
|
handleExecuteQuickActionOnClick,
|
||||||
isRemoteObject,
|
|
||||||
setActionBarEntriesState,
|
setActionBarEntriesState,
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
|||||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||||
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
|
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
|
||||||
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
|
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
|
||||||
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
|
import { useViewStates } from '@/views/hooks/internal/useViewStates';
|
||||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||||
|
|
||||||
type RecordIndexTableContainerEffectProps = {
|
type RecordIndexTableContainerEffectProps = {
|
||||||
@ -45,12 +47,31 @@ export const RecordIndexTableContainerEffect = ({
|
|||||||
setAvailableTableColumns(columnDefinitions);
|
setAvailableTableColumns(columnDefinitions);
|
||||||
}, [columnDefinitions, setAvailableTableColumns]);
|
}, [columnDefinitions, setAvailableTableColumns]);
|
||||||
|
|
||||||
|
const { tableRowIdsState, hasUserSelectedAllRowsState } =
|
||||||
|
useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
|
const { entityCountInCurrentViewState } = useViewStates(recordTableId);
|
||||||
|
const entityCountInCurrentView = useRecoilValue(
|
||||||
|
entityCountInCurrentViewState,
|
||||||
|
);
|
||||||
|
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||||
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||||
|
|
||||||
|
const numSelected =
|
||||||
|
hasUserSelectedAllRows && entityCountInCurrentView
|
||||||
|
? selectedRowIds.length === tableRowIds.length
|
||||||
|
? entityCountInCurrentView
|
||||||
|
: entityCountInCurrentView -
|
||||||
|
(tableRowIds.length - selectedRowIds.length) // unselected row Ids
|
||||||
|
: selectedRowIds.length;
|
||||||
|
|
||||||
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
selectedRecordIds: selectedRowIds,
|
selectedRecordIds: selectedRowIds,
|
||||||
callback: resetTableRowSelection,
|
callback: resetTableRowSelection,
|
||||||
|
totalNumberOfRecordsSelected: numSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleColumnFilter = useHandleToggleColumnFilter({
|
const handleToggleColumnFilter = useHandleToggleColumnFilter({
|
||||||
|
|||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
|
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
|
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
||||||
|
import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData';
|
||||||
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
|
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||||
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
type UseDeleteTableDataOptions = Omit<UseTableDataOptions, 'callback'>;
|
||||||
|
|
||||||
|
export const useDeleteTableData = ({
|
||||||
|
objectNameSingular,
|
||||||
|
recordIndexId,
|
||||||
|
}: UseDeleteTableDataOptions) => {
|
||||||
|
const { fetchAllRecordIds } = useFetchAllRecordIds({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
resetTableRowSelection,
|
||||||
|
selectedRowIdsSelector,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
|
} = useRecordTable({
|
||||||
|
recordTableId: recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableRowIds = useRecoilValue(
|
||||||
|
tableRowIdsComponentState({
|
||||||
|
scopeId: getScopeIdFromComponentId(recordIndexId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { deleteManyRecords } = useDeleteManyRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
const { favorites, deleteFavorite } = useFavorites();
|
||||||
|
|
||||||
|
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||||
|
|
||||||
|
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||||
|
|
||||||
|
const deleteRecords = async () => {
|
||||||
|
let recordIdsToDelete = selectedRowIds;
|
||||||
|
|
||||||
|
if (hasUserSelectedAllRows) {
|
||||||
|
const allRecordIds = await fetchAllRecordIds();
|
||||||
|
|
||||||
|
const unselectedRecordIds = tableRowIds.filter(
|
||||||
|
(recordId) => !selectedRowIds.includes(recordId),
|
||||||
|
);
|
||||||
|
|
||||||
|
recordIdsToDelete = allRecordIds.filter(
|
||||||
|
(recordId) => !unselectedRecordIds.includes(recordId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTableRowSelection();
|
||||||
|
|
||||||
|
for (const recordIdToDelete of recordIdsToDelete) {
|
||||||
|
const foundFavorite = favorites?.find(
|
||||||
|
(favorite) => favorite.recordId === recordIdToDelete,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundFavorite !== undefined) {
|
||||||
|
deleteFavorite(foundFavorite.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteManyRecords(recordIdsToDelete, {
|
||||||
|
delayInMsBetweenRequests: 50,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { deleteTableData: deleteRecords };
|
||||||
|
};
|
||||||
@ -1,17 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { json2csv } from 'json-2-csv';
|
import { json2csv } from 'json-2-csv';
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import {
|
||||||
|
useTableData,
|
||||||
|
UseTableDataOptions,
|
||||||
|
} from '@/object-record/record-index/options/hooks/useTableData';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
import { sleep } from '~/utils/sleep';
|
|
||||||
|
|
||||||
import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable';
|
|
||||||
|
|
||||||
export const download = (blob: Blob, filename: string) => {
|
export const download = (blob: Blob, filename: string) => {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@ -127,13 +126,8 @@ const downloader = (mimeType: string, generator: GenerateExport) => {
|
|||||||
|
|
||||||
export const csvDownloader = downloader('text/csv', generateCsv);
|
export const csvDownloader = downloader('text/csv', generateCsv);
|
||||||
|
|
||||||
type UseExportTableDataOptions = {
|
type UseExportTableDataOptions = Omit<UseTableDataOptions, 'callback'> & {
|
||||||
delayMs: number;
|
|
||||||
filename: string;
|
filename: string;
|
||||||
maximumRequests?: number;
|
|
||||||
objectNameSingular: string;
|
|
||||||
pageSize?: number;
|
|
||||||
recordIndexId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useExportTableData = ({
|
export const useExportTableData = ({
|
||||||
@ -144,100 +138,22 @@ export const useExportTableData = ({
|
|||||||
pageSize = 30,
|
pageSize = 30,
|
||||||
recordIndexId,
|
recordIndexId,
|
||||||
}: UseExportTableDataOptions) => {
|
}: UseExportTableDataOptions) => {
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const downloadCsv = useMemo(
|
||||||
const [inflight, setInflight] = useState(false);
|
() =>
|
||||||
const [pageCount, setPageCount] = useState(0);
|
(rows: ObjectRecord[], columns: ColumnDefinition<FieldMetadata>[]) => {
|
||||||
const [progress, setProgress] = useState<ExportProgress>({
|
csvDownloader(filename, { rows, columns });
|
||||||
displayType: 'number',
|
},
|
||||||
});
|
[filename],
|
||||||
const [previousRecordCount, setPreviousRecordCount] = useState(0);
|
|
||||||
|
|
||||||
const { visibleTableColumnsSelector, selectedRowIdsSelector } =
|
|
||||||
useRecordTableStates(recordIndexId);
|
|
||||||
|
|
||||||
const columns = useRecoilValue(visibleTableColumnsSelector());
|
|
||||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
|
||||||
|
|
||||||
const hasSelectedRows = selectedRowIds.length > 0;
|
|
||||||
|
|
||||||
const findManyRecordsParams = useFindManyParams(
|
|
||||||
objectNameSingular,
|
|
||||||
recordIndexId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedFindManyParams = {
|
const { getTableData: download, progress } = useTableData({
|
||||||
...findManyRecordsParams,
|
delayMs,
|
||||||
filter: {
|
maximumRequests,
|
||||||
...findManyRecordsParams.filter,
|
objectNameSingular,
|
||||||
id: {
|
pageSize,
|
||||||
in: selectedRowIds,
|
recordIndexId,
|
||||||
},
|
callback: downloadCsv,
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const usedFindManyParams = hasSelectedRows
|
|
||||||
? selectedFindManyParams
|
|
||||||
: findManyRecordsParams;
|
|
||||||
|
|
||||||
// Todo: this needs to be done on click on the Export not button, not to be reactive. Use Lazy query for example
|
|
||||||
const { totalCount, records, fetchMoreRecords } = useFindManyRecords({
|
|
||||||
...usedFindManyParams,
|
|
||||||
limit: pageSize,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
return { progress, download };
|
||||||
const MAXIMUM_REQUESTS = isDefined(totalCount)
|
|
||||||
? Math.min(maximumRequests, totalCount / pageSize)
|
|
||||||
: maximumRequests;
|
|
||||||
|
|
||||||
const downloadCsv = (rows: object[]) => {
|
|
||||||
csvDownloader(filename, { rows, columns });
|
|
||||||
setIsDownloading(false);
|
|
||||||
setProgress({
|
|
||||||
displayType: 'number',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchNextPage = async () => {
|
|
||||||
setInflight(true);
|
|
||||||
setPreviousRecordCount(records.length);
|
|
||||||
await fetchMoreRecords();
|
|
||||||
setPageCount((state) => state + 1);
|
|
||||||
setProgress({
|
|
||||||
exportedRecordCount: records.length,
|
|
||||||
totalRecordCount: totalCount,
|
|
||||||
displayType: totalCount ? 'percentage' : 'number',
|
|
||||||
});
|
|
||||||
await sleep(delayMs);
|
|
||||||
setInflight(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isDownloading || inflight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
pageCount >= MAXIMUM_REQUESTS ||
|
|
||||||
records.length === previousRecordCount
|
|
||||||
) {
|
|
||||||
downloadCsv(records);
|
|
||||||
} else {
|
|
||||||
fetchNextPage();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
delayMs,
|
|
||||||
fetchMoreRecords,
|
|
||||||
filename,
|
|
||||||
inflight,
|
|
||||||
isDownloading,
|
|
||||||
pageCount,
|
|
||||||
records,
|
|
||||||
totalCount,
|
|
||||||
columns,
|
|
||||||
maximumRequests,
|
|
||||||
pageSize,
|
|
||||||
previousRecordCount,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { progress, download: () => setIsDownloading(true) };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,204 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable';
|
||||||
|
|
||||||
|
export const sleep = (ms: number) =>
|
||||||
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export const percentage = (part: number, whole: number): number => {
|
||||||
|
return Math.round((part / whole) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseTableDataOptions = {
|
||||||
|
delayMs: number;
|
||||||
|
maximumRequests?: number;
|
||||||
|
objectNameSingular: string;
|
||||||
|
pageSize?: number;
|
||||||
|
recordIndexId: string;
|
||||||
|
callback: (
|
||||||
|
rows: ObjectRecord[],
|
||||||
|
columns: ColumnDefinition<FieldMetadata>[],
|
||||||
|
) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExportProgress = {
|
||||||
|
exportedRecordCount?: number;
|
||||||
|
totalRecordCount?: number;
|
||||||
|
displayType: 'percentage' | 'number';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTableData = ({
|
||||||
|
delayMs,
|
||||||
|
maximumRequests = 100,
|
||||||
|
objectNameSingular,
|
||||||
|
pageSize = 30,
|
||||||
|
recordIndexId,
|
||||||
|
callback,
|
||||||
|
}: UseTableDataOptions) => {
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const [inflight, setInflight] = useState(false);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [progress, setProgress] = useState<ExportProgress>({
|
||||||
|
displayType: 'number',
|
||||||
|
});
|
||||||
|
const [previousRecordCount, setPreviousRecordCount] = useState(0);
|
||||||
|
|
||||||
|
const {
|
||||||
|
visibleTableColumnsSelector,
|
||||||
|
selectedRowIdsSelector,
|
||||||
|
tableRowIdsState,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
|
} = useRecordTableStates(recordIndexId);
|
||||||
|
|
||||||
|
const columns = useRecoilValue(visibleTableColumnsSelector());
|
||||||
|
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||||
|
|
||||||
|
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||||
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
|
|
||||||
|
// user has checked select all and then unselected some rows
|
||||||
|
const userHasUnselectedSomeRows =
|
||||||
|
hasUserSelectedAllRows && selectedRowIds.length < tableRowIds.length;
|
||||||
|
|
||||||
|
const hasSelectedRows =
|
||||||
|
selectedRowIds.length > 0 &&
|
||||||
|
!(hasUserSelectedAllRows && selectedRowIds.length === tableRowIds.length);
|
||||||
|
|
||||||
|
const unselectedRowIds = useMemo(
|
||||||
|
() =>
|
||||||
|
userHasUnselectedSomeRows
|
||||||
|
? tableRowIds.filter((id) => !selectedRowIds.includes(id))
|
||||||
|
: [],
|
||||||
|
[userHasUnselectedSomeRows, tableRowIds, selectedRowIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const findManyRecordsParams = useFindManyParams(
|
||||||
|
objectNameSingular,
|
||||||
|
recordIndexId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFindManyParams = {
|
||||||
|
...findManyRecordsParams,
|
||||||
|
filter: {
|
||||||
|
...findManyRecordsParams.filter,
|
||||||
|
id: {
|
||||||
|
in: selectedRowIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const unselectedFindManyParams = {
|
||||||
|
...findManyRecordsParams,
|
||||||
|
filter: {
|
||||||
|
...findManyRecordsParams.filter,
|
||||||
|
not: {
|
||||||
|
id: {
|
||||||
|
in: unselectedRowIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const usedFindManyParams =
|
||||||
|
hasSelectedRows && !userHasUnselectedSomeRows
|
||||||
|
? selectedFindManyParams
|
||||||
|
: userHasUnselectedSomeRows
|
||||||
|
? unselectedFindManyParams
|
||||||
|
: findManyRecordsParams;
|
||||||
|
|
||||||
|
const {
|
||||||
|
findManyRecords,
|
||||||
|
totalCount,
|
||||||
|
records,
|
||||||
|
fetchMoreRecordsWithPagination,
|
||||||
|
loading,
|
||||||
|
} = useLazyFindManyRecords({
|
||||||
|
...usedFindManyParams,
|
||||||
|
limit: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const MAXIMUM_REQUESTS = isDefined(totalCount)
|
||||||
|
? Math.min(maximumRequests, totalCount / pageSize)
|
||||||
|
: maximumRequests;
|
||||||
|
|
||||||
|
const fetchNextPage = async () => {
|
||||||
|
setInflight(true);
|
||||||
|
setPreviousRecordCount(records.length);
|
||||||
|
|
||||||
|
await fetchMoreRecordsWithPagination();
|
||||||
|
|
||||||
|
setPageCount((state) => state + 1);
|
||||||
|
setProgress({
|
||||||
|
exportedRecordCount: records.length,
|
||||||
|
totalRecordCount: totalCount,
|
||||||
|
displayType: totalCount ? 'percentage' : 'number',
|
||||||
|
});
|
||||||
|
await sleep(delayMs);
|
||||||
|
setInflight(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isDownloading || inflight || loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pageCount >= MAXIMUM_REQUESTS ||
|
||||||
|
(isDefined(totalCount) && records.length === totalCount)
|
||||||
|
) {
|
||||||
|
setPageCount(0);
|
||||||
|
|
||||||
|
const complete = () => {
|
||||||
|
setPageCount(0);
|
||||||
|
setPreviousRecordCount(0);
|
||||||
|
setIsDownloading(false);
|
||||||
|
setProgress({
|
||||||
|
displayType: 'number',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = callback(records, columns);
|
||||||
|
|
||||||
|
if (res instanceof Promise) {
|
||||||
|
res.then(complete);
|
||||||
|
} else {
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
delayMs,
|
||||||
|
fetchMoreRecordsWithPagination,
|
||||||
|
inflight,
|
||||||
|
isDownloading,
|
||||||
|
pageCount,
|
||||||
|
records,
|
||||||
|
totalCount,
|
||||||
|
columns,
|
||||||
|
maximumRequests,
|
||||||
|
pageSize,
|
||||||
|
loading,
|
||||||
|
callback,
|
||||||
|
previousRecordCount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
progress,
|
||||||
|
isDownloading,
|
||||||
|
getTableData: () => {
|
||||||
|
setPageCount(0);
|
||||||
|
setPreviousRecordCount(0);
|
||||||
|
setIsDownloading(true);
|
||||||
|
findManyRecords?.();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -2,19 +2,43 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
|
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
|
||||||
|
import { useViewStates } from '@/views/hooks/internal/useViewStates';
|
||||||
|
|
||||||
export const RecordTableActionBar = ({
|
export const RecordTableActionBar = ({
|
||||||
recordTableId,
|
recordTableId,
|
||||||
}: {
|
}: {
|
||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { selectedRowIdsSelector } = useRecordTableStates(recordTableId);
|
const {
|
||||||
|
selectedRowIdsSelector,
|
||||||
|
tableRowIdsState,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
|
} = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
|
const { entityCountInCurrentViewState } = useViewStates(recordTableId);
|
||||||
|
const entityCountInCurrentView = useRecoilValue(
|
||||||
|
entityCountInCurrentViewState,
|
||||||
|
);
|
||||||
|
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||||
|
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||||
|
|
||||||
|
const totalNumberOfSelectedRecords =
|
||||||
|
hasUserSelectedAllRows && entityCountInCurrentView
|
||||||
|
? selectedRowIds.length === tableRowIds.length
|
||||||
|
? entityCountInCurrentView
|
||||||
|
: entityCountInCurrentView -
|
||||||
|
(tableRowIds.length - selectedRowIds.length) // unselected row Ids
|
||||||
|
: selectedRowIds.length;
|
||||||
|
|
||||||
if (!selectedRowIds.length) {
|
if (!selectedRowIds.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ActionBar selectedIds={selectedRowIds} />;
|
return (
|
||||||
|
<ActionBar
|
||||||
|
selectedIds={selectedRowIds}
|
||||||
|
totalNumberOfSelectedRecords={totalNumberOfSelectedRecords}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
|
|||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import {
|
||||||
|
ClickOutsideMode,
|
||||||
|
useListenClickOutsideByClassName,
|
||||||
|
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
|
||||||
type RecordTableInternalEffectProps = {
|
type RecordTableInternalEffectProps = {
|
||||||
recordTableId: string;
|
recordTableId: string;
|
||||||
@ -30,6 +33,7 @@ export const RecordTableInternalEffect = ({
|
|||||||
callback: () => {
|
callback: () => {
|
||||||
leaveTableFocus();
|
leaveTableFocus();
|
||||||
},
|
},
|
||||||
|
mode: ClickOutsideMode.comparePixels,
|
||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
|
|||||||
@ -8,12 +8,17 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
|||||||
|
|
||||||
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
|
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
|
||||||
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
import { useDisableSoftFocus } from './useDisableSoftFocus';
|
||||||
|
import { useSetHasUserSelectedAllRows } from './useSetAllRowSelectedState';
|
||||||
|
|
||||||
export const useLeaveTableFocus = (recordTableId?: string) => {
|
export const useLeaveTableFocus = (recordTableId?: string) => {
|
||||||
const disableSoftFocus = useDisableSoftFocus(recordTableId);
|
const disableSoftFocus = useDisableSoftFocus(recordTableId);
|
||||||
const closeCurrentCellInEditMode =
|
const closeCurrentCellInEditMode =
|
||||||
useCloseCurrentTableCellInEditMode(recordTableId);
|
useCloseCurrentTableCellInEditMode(recordTableId);
|
||||||
|
|
||||||
|
const setHasUserSelectedAllRows = useSetHasUserSelectedAllRows(recordTableId);
|
||||||
|
|
||||||
|
const selectAllRows = useSetHasUserSelectedAllRows(recordTableId);
|
||||||
|
|
||||||
const { isSoftFocusActiveState } = useRecordTableStates(recordTableId);
|
const { isSoftFocusActiveState } = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
@ -38,7 +43,15 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
|
|||||||
|
|
||||||
closeCurrentCellInEditMode();
|
closeCurrentCellInEditMode();
|
||||||
disableSoftFocus();
|
disableSoftFocus();
|
||||||
|
setHasUserSelectedAllRows(false);
|
||||||
|
selectAllRows(false);
|
||||||
},
|
},
|
||||||
[closeCurrentCellInEditMode, disableSoftFocus, isSoftFocusActiveState],
|
[
|
||||||
|
closeCurrentCellInEditMode,
|
||||||
|
disableSoftFocus,
|
||||||
|
isSoftFocusActiveState,
|
||||||
|
selectAllRows,
|
||||||
|
setHasUserSelectedAllRows,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -109,7 +109,7 @@ export const useRecordTableStates = (recordTableId?: string) => {
|
|||||||
isRowSelectedComponentFamilyState,
|
isRowSelectedComponentFamilyState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
hasUserSelectedAllRowState: extractComponentState(
|
hasUserSelectedAllRowsState: extractComponentState(
|
||||||
hasUserSelectedAllRowsComponentState,
|
hasUserSelectedAllRowsComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -4,8 +4,11 @@ import { useRecordTableStates } from '@/object-record/record-table/hooks/interna
|
|||||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||||
|
|
||||||
export const useResetTableRowSelection = (recordTableId?: string) => {
|
export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||||
const { tableRowIdsState, isRowSelectedFamilyState } =
|
const {
|
||||||
useRecordTableStates(recordTableId);
|
tableRowIdsState,
|
||||||
|
isRowSelectedFamilyState,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
|
} = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
({ snapshot, set }) =>
|
||||||
@ -15,7 +18,9 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
|||||||
for (const rowId of tableRowIds) {
|
for (const rowId of tableRowIds) {
|
||||||
set(isRowSelectedFamilyState(rowId), false);
|
set(isRowSelectedFamilyState(rowId), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set(hasUserSelectedAllRowsState, false);
|
||||||
},
|
},
|
||||||
[tableRowIdsState, isRowSelectedFamilyState],
|
[tableRowIdsState, isRowSelectedFamilyState, hasUserSelectedAllRowsState],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,14 +3,13 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||||
|
|
||||||
export const useSetHasUserSelectedAllRows = (recordTableId?: string) => {
|
export const useSetHasUserSelectedAllRows = (recordTableId?: string) => {
|
||||||
const { hasUserSelectedAllRowState: hasUserSelectedAllRowFamilyState } =
|
const { hasUserSelectedAllRowsState } = useRecordTableStates(recordTableId);
|
||||||
useRecordTableStates(recordTableId);
|
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(selected: boolean) => {
|
(selected: boolean) => {
|
||||||
set(hasUserSelectedAllRowFamilyState, selected);
|
set(hasUserSelectedAllRowsState, selected);
|
||||||
},
|
},
|
||||||
[hasUserSelectedAllRowFamilyState],
|
[hasUserSelectedAllRowsState],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const useSetRecordTableData = ({
|
|||||||
tableRowIdsState,
|
tableRowIdsState,
|
||||||
numberOfTableRowsState,
|
numberOfTableRowsState,
|
||||||
isRowSelectedFamilyState,
|
isRowSelectedFamilyState,
|
||||||
hasUserSelectedAllRowState,
|
hasUserSelectedAllRowsState,
|
||||||
} = useRecordTableStates(recordTableId);
|
} = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
return useRecoilCallback(
|
return useRecoilCallback(
|
||||||
@ -39,7 +39,7 @@ export const useSetRecordTableData = ({
|
|||||||
|
|
||||||
const hasUserSelectedAllRows = getSnapshotValue(
|
const hasUserSelectedAllRows = getSnapshotValue(
|
||||||
snapshot,
|
snapshot,
|
||||||
hasUserSelectedAllRowState,
|
hasUserSelectedAllRowsState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const entityIds = newEntityArray.map((entity) => entity.id);
|
const entityIds = newEntityArray.map((entity) => entity.id);
|
||||||
@ -62,7 +62,7 @@ export const useSetRecordTableData = ({
|
|||||||
tableRowIdsState,
|
tableRowIdsState,
|
||||||
onEntityCountChange,
|
onEntityCountChange,
|
||||||
isRowSelectedFamilyState,
|
isRowSelectedFamilyState,
|
||||||
hasUserSelectedAllRowState,
|
hasUserSelectedAllRowsState,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
onToggleColumnFilterState,
|
onToggleColumnFilterState,
|
||||||
onToggleColumnSortState,
|
onToggleColumnSortState,
|
||||||
pendingRecordIdState,
|
pendingRecordIdState,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
} = useRecordTableStates(recordTableId);
|
} = useRecordTableStates(recordTableId);
|
||||||
|
|
||||||
const setAvailableTableColumns = useRecoilCallback(
|
const setAvailableTableColumns = useRecoilCallback(
|
||||||
@ -226,5 +227,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
|
|||||||
setOnToggleColumnFilter,
|
setOnToggleColumnFilter,
|
||||||
setOnToggleColumnSort,
|
setOnToggleColumnSort,
|
||||||
setPendingRecordId,
|
setPendingRecordId,
|
||||||
|
hasUserSelectedAllRowsState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { numberOfTableRowsComponentState } from '@/object-record/record-table/states/numberOfTableRowsComponentState';
|
|
||||||
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
|
||||||
|
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||||
import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector';
|
import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector';
|
||||||
|
|
||||||
import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus';
|
import { AllRowsSelectedStatus } from '../../types/AllRowSelectedStatus';
|
||||||
@ -10,7 +10,7 @@ export const allRowsSelectedStatusComponentSelector =
|
|||||||
get:
|
get:
|
||||||
({ scopeId }) =>
|
({ scopeId }) =>
|
||||||
({ get }) => {
|
({ get }) => {
|
||||||
const numberOfRows = get(numberOfTableRowsComponentState({ scopeId }));
|
const tableRowIds = get(tableRowIdsComponentState({ scopeId }));
|
||||||
|
|
||||||
const selectedRowIds = get(
|
const selectedRowIds = get(
|
||||||
selectedRowIdsComponentSelector({ scopeId }),
|
selectedRowIdsComponentSelector({ scopeId }),
|
||||||
@ -21,7 +21,7 @@ export const allRowsSelectedStatusComponentSelector =
|
|||||||
const allRowsSelectedStatus =
|
const allRowsSelectedStatus =
|
||||||
numberOfSelectedRows === 0
|
numberOfSelectedRows === 0
|
||||||
? 'none'
|
? 'none'
|
||||||
: numberOfRows === numberOfSelectedRows
|
: selectedRowIds.length === tableRowIds.length
|
||||||
? 'all'
|
? 'all'
|
||||||
: 'some';
|
: 'some';
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
|
||||||
|
export type OnFindManyRecordsCompleted<T> = (
|
||||||
|
records: T[],
|
||||||
|
options?: {
|
||||||
|
pageInfo?: RecordGqlConnection['pageInfo'];
|
||||||
|
totalCount?: number;
|
||||||
|
},
|
||||||
|
) => void;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { WatchQueryFetchPolicy } from '@apollo/client';
|
||||||
|
|
||||||
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
|
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||||
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
|
import { OnFindManyRecordsCompleted } from '@/object-record/types/OnFindManyRecordsCompleted';
|
||||||
|
|
||||||
|
export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
|
||||||
|
RecordGqlOperationVariables & {
|
||||||
|
onCompleted?: OnFindManyRecordsCompleted<T>;
|
||||||
|
skip?: boolean;
|
||||||
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
fetchPolicy?: WatchQueryFetchPolicy;
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
|
|
||||||
|
export const getQueryIdentifier = ({
|
||||||
|
objectNameSingular,
|
||||||
|
filter,
|
||||||
|
orderBy,
|
||||||
|
limit,
|
||||||
|
}: RecordGqlOperationVariables & {
|
||||||
|
objectNameSingular: string;
|
||||||
|
}) =>
|
||||||
|
objectNameSingular + JSON.stringify(filter) + JSON.stringify(orderBy) + limit;
|
||||||
@ -1,15 +1,17 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
|
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
|
||||||
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
||||||
import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
|
import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
|
||||||
|
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { ActionBarItem } from './ActionBarItem';
|
import { ActionBarItem } from './ActionBarItem';
|
||||||
|
|
||||||
type ActionBarProps = {
|
type ActionBarProps = {
|
||||||
selectedIds?: string[];
|
selectedIds?: string[];
|
||||||
|
totalNumberOfSelectedRecords?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainerActionBar = styled.div`
|
const StyledContainerActionBar = styled.div`
|
||||||
@ -40,7 +42,10 @@ const StyledLabel = styled.div`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionBar = ({ selectedIds = [] }: ActionBarProps) => {
|
export const ActionBar = ({
|
||||||
|
selectedIds = [],
|
||||||
|
totalNumberOfSelectedRecords,
|
||||||
|
}: ActionBarProps) => {
|
||||||
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,6 +62,12 @@ export const ActionBar = ({ selectedIds = [] }: ActionBarProps) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedNumberLabel =
|
||||||
|
totalNumberOfSelectedRecords ?? selectedIds?.length;
|
||||||
|
|
||||||
|
const showSelectedNumberLabel =
|
||||||
|
isDefined(totalNumberOfSelectedRecords) || Array.isArray(selectedIds);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledContainerActionBar
|
<StyledContainerActionBar
|
||||||
@ -64,8 +75,8 @@ export const ActionBar = ({ selectedIds = [] }: ActionBarProps) => {
|
|||||||
className="action-bar"
|
className="action-bar"
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
>
|
>
|
||||||
{selectedIds && (
|
{showSelectedNumberLabel && (
|
||||||
<StyledLabel>{selectedIds.length} selected:</StyledLabel>
|
<StyledLabel>{selectedNumberLabel} selected:</StyledLabel>
|
||||||
)}
|
)}
|
||||||
{actionBarEntries.map((item, index) => (
|
{actionBarEntries.map((item, index) => (
|
||||||
<ActionBarItem key={index} item={item} />
|
<ActionBarItem key={index} item={item} />
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import { IconComponent } from 'twenty-ui';
|
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
|
||||||
|
|
||||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
export type ActionBarEntry = ContextMenuEntry & {
|
||||||
|
|
||||||
export type ActionBarEntry = {
|
|
||||||
label: string;
|
|
||||||
Icon: IconComponent;
|
|
||||||
accent?: MenuItemAccent;
|
|
||||||
onClick?: () => void;
|
|
||||||
subActions?: ActionBarEntry[];
|
subActions?: ActionBarEntry[];
|
||||||
ConfirmationModal?: JSX.Element;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
||||||
@ -6,5 +7,6 @@ export type ContextMenuEntry = {
|
|||||||
label: string;
|
label: string;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
accent?: MenuItemAccent;
|
accent?: MenuItemAccent;
|
||||||
onClick: () => void;
|
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||||
|
ConfirmationModal?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
|
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/standard-metadata-query-result';
|
||||||
|
|
||||||
|
// TODO: replace with new mock
|
||||||
const customObjectMetadataItemEdge: ObjectEdge = {
|
const customObjectMetadataItemEdge: ObjectEdge = {
|
||||||
__typename: 'objectEdge',
|
__typename: 'objectEdge',
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
|
||||||
export const getPeopleMock = () => {
|
export const getPeopleMock = () => {
|
||||||
const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node);
|
const peopleMock = peopleQueryResult.people.edges.map((edge) => edge.node);
|
||||||
|
|
||||||
@ -22,7 +24,7 @@ export const mockedEmptyPersonData = {
|
|||||||
__typename: 'Person',
|
__typename: 'Person',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const peopleQueryResult = {
|
export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||||
people: {
|
people: {
|
||||||
__typename: 'PersonConnection',
|
__typename: 'PersonConnection',
|
||||||
totalCount: 15,
|
totalCount: 15,
|
||||||
|
|||||||
Reference in New Issue
Block a user