[FEAT] RecordAction destroy many record (#9991)
# Introduction Added the `RecordAction` destroy multiple record ## Repro Select multiples `deletedRecords`, you should be able to see the `Destroy` pinned CTA ( iso short label with the destroy one ), open control panel and fin new CTA `Permanently delete records` https://github.com/user-attachments/assets/31ee8738-9d61-4dec-9a1f-41bb6785e018 ## TODO - [ ] Gain granularity within tests to assert the action should be registered only when filtering by deleted ## Conclusion Closes https://github.com/twentyhq/core-team-issues/issues/110
This commit is contained in:
@ -1,4 +1,5 @@
|
|||||||
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
|
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
|
||||||
|
import { useDestroyMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDestroyMultipleRecordsAction';
|
||||||
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
|
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
|
||||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||||
import { useCreateNewTableRecordNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useCreateNewTableRecordNoSelectionRecordAction';
|
import { useCreateNewTableRecordNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useCreateNewTableRecordNoSelectionRecordAction';
|
||||||
@ -184,4 +185,17 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
|||||||
availableOn: [ActionViewType.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
useAction: useNavigateToNextRecordSingleRecordAction,
|
useAction: useNavigateToNextRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
|
destroyMultipleRecords: {
|
||||||
|
type: ActionMenuEntryType.Standard,
|
||||||
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
|
key: MultipleRecordsActionKeys.DESTROY,
|
||||||
|
label: msg`Permanently destroy records`,
|
||||||
|
shortLabel: msg`Destroy`,
|
||||||
|
position: 10,
|
||||||
|
Icon: IconTrashX,
|
||||||
|
accent: 'danger',
|
||||||
|
isPinned: true,
|
||||||
|
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||||
|
useAction: useDestroyMultipleRecordsAction,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,145 @@
|
|||||||
|
import { DestroyManyRecordsProps } from '@/object-record/hooks/useDestroyManyRecords';
|
||||||
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { expect } from '@storybook/test';
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { act } from 'react';
|
||||||
|
import {
|
||||||
|
GetJestMetadataAndApolloMocksAndActionMenuWrapperProps,
|
||||||
|
getJestMetadataAndApolloMocksAndActionMenuWrapper,
|
||||||
|
} from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
||||||
|
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||||
|
import { getPeopleMock } from '~/testing/mock-data/people';
|
||||||
|
import { useDestroyMultipleRecordsAction } from '../useDestroyMultipleRecordsAction';
|
||||||
|
|
||||||
|
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === 'person',
|
||||||
|
)!;
|
||||||
|
const personMockObjectMetadataItemDeletedAtField =
|
||||||
|
personMockObjectMetadataItem.fields.find((el) => el.name === 'deletedAt');
|
||||||
|
if (personMockObjectMetadataItemDeletedAtField === undefined)
|
||||||
|
throw new Error('Should never occurs');
|
||||||
|
|
||||||
|
const [firstPeopleMock, secondPeopleMock] = getPeopleMock().map((record) => ({
|
||||||
|
...record,
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const destroyManyRecordsMock = jest.fn();
|
||||||
|
const resetTableRowSelectionMock = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('@/object-record/hooks/useDestroyManyRecords', () => ({
|
||||||
|
useDestroyManyRecords: () => ({
|
||||||
|
destroyManyRecords: destroyManyRecordsMock,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/object-record/hooks/useLazyFetchAllRecords', () => ({
|
||||||
|
useLazyFetchAllRecords: () => {
|
||||||
|
return {
|
||||||
|
fetchAllRecords: () => [firstPeopleMock, secondPeopleMock],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
|
||||||
|
useRecordTable: () => ({
|
||||||
|
resetTableRowSelection: resetTableRowSelectionMock,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getWrapper = (
|
||||||
|
overrides?: Partial<GetJestMetadataAndApolloMocksAndActionMenuWrapperProps>,
|
||||||
|
) =>
|
||||||
|
getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||||
|
apolloMocks: [],
|
||||||
|
componentInstanceId: '1',
|
||||||
|
contextStoreCurrentObjectMetadataNameSingular:
|
||||||
|
personMockObjectMetadataItem.nameSingular,
|
||||||
|
contextStoreTargetedRecordsRule: {
|
||||||
|
mode: 'selection',
|
||||||
|
selectedRecordIds: [firstPeopleMock.id, secondPeopleMock.id],
|
||||||
|
},
|
||||||
|
contextStoreFilters: [],
|
||||||
|
contextStoreNumberOfSelectedRecords: 2,
|
||||||
|
onInitializeRecoilSnapshot: (snapshot) => {
|
||||||
|
snapshot.set(recordStoreFamilyState(firstPeopleMock.id), firstPeopleMock);
|
||||||
|
snapshot.set(
|
||||||
|
recordStoreFamilyState(secondPeopleMock.id),
|
||||||
|
secondPeopleMock,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultWrapper = getWrapper();
|
||||||
|
|
||||||
|
describe('useDestroyMultipleRecordsAction', () => {
|
||||||
|
it('should call destroyManyRecords on click if records are filtered by deletedAt', async () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() =>
|
||||||
|
useDestroyMultipleRecordsAction({
|
||||||
|
objectMetadataItem: personMockObjectMetadataItem,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
wrapper: getWrapper({
|
||||||
|
contextStoreFilters: [
|
||||||
|
{
|
||||||
|
id: '1553cda7-893d-4d89-b7ab-04969a4c2927',
|
||||||
|
fieldMetadataId: personMockObjectMetadataItemDeletedAtField.id,
|
||||||
|
value: '',
|
||||||
|
displayValue: '',
|
||||||
|
operand: ViewFilterOperand.IsNotEmpty,
|
||||||
|
definition: {
|
||||||
|
label: 'Deleted',
|
||||||
|
iconName: 'IconTrash',
|
||||||
|
fieldMetadataId: personMockObjectMetadataItemDeletedAtField.id,
|
||||||
|
type: 'DATE_TIME',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.ConfirmationModal?.props?.isOpen).toBeFalsy();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.onClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.ConfirmationModal?.props?.isOpen).toBe(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.ConfirmationModal?.props?.onConfirmClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedParams: DestroyManyRecordsProps = {
|
||||||
|
recordIdsToDestroy: [firstPeopleMock.id, secondPeopleMock.id],
|
||||||
|
};
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resetTableRowSelectionMock).toHaveBeenCalled();
|
||||||
|
expect(destroyManyRecordsMock).toHaveBeenCalledWith(expectedParams);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call destroyManyRecords on click if records are not filtered by deletedAt', async () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() =>
|
||||||
|
useDestroyMultipleRecordsAction({
|
||||||
|
objectMetadataItem: personMockObjectMetadataItem,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
wrapper: defaultWrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.ConfirmationModal?.props?.isOpen).toBeFalsy();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.onClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.ConfirmationModal?.props?.isOpen).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -4,8 +4,8 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
|
|||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
|
import { BACKEND_BATCH_REQUEST_MAX_COUNT } from '@/object-record/constants/BackendBatchRequestMaxCount';
|
||||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||||
@ -84,7 +84,7 @@ export const useDeleteMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
|||||||
!isRemoteObject &&
|
!isRemoteObject &&
|
||||||
!isDeletedFilterActive &&
|
!isDeletedFilterActive &&
|
||||||
isDefined(contextStoreNumberOfSelectedRecords) &&
|
isDefined(contextStoreNumberOfSelectedRecords) &&
|
||||||
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
contextStoreNumberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT &&
|
||||||
contextStoreNumberOfSelectedRecords > 0;
|
contextStoreNumberOfSelectedRecords > 0;
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
|
||||||
|
|
||||||
|
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||||
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
|
import { BACKEND_BATCH_REQUEST_MAX_COUNT } from '@/object-record/constants/BackendBatchRequestMaxCount';
|
||||||
|
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||||
|
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
|
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
|
||||||
|
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||||
|
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
|
||||||
|
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||||
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const useDestroyMultipleRecordsAction: ActionHookWithObjectMetadataItem =
|
||||||
|
({ objectMetadataItem }) => {
|
||||||
|
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const { resetTableRowSelection } = useRecordTable({
|
||||||
|
recordTableId: objectMetadataItem.namePlural,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { destroyManyRecords } = useDestroyManyRecords({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||||
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextStoreFilters = useRecoilComponentValueV2(
|
||||||
|
contextStoreFiltersComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { filterValueDependencies } = useFilterValueDependencies();
|
||||||
|
|
||||||
|
const deletedAtFilter: RecordGqlOperationFilter = {
|
||||||
|
deletedAt: { is: 'NOT_NULL' },
|
||||||
|
};
|
||||||
|
const graphqlFilter = {
|
||||||
|
...computeContextStoreFilters(
|
||||||
|
contextStoreTargetedRecordsRule,
|
||||||
|
contextStoreFilters,
|
||||||
|
objectMetadataItem,
|
||||||
|
filterValueDependencies,
|
||||||
|
),
|
||||||
|
...deletedAtFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletedAtFieldMetadata = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.name === 'deletedAt',
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDeletedFilterActive = contextStoreFilters.some(
|
||||||
|
(filter) =>
|
||||||
|
filter.fieldMetadataId === deletedAtFieldMetadata?.id &&
|
||||||
|
filter.operand === RecordFilterOperand.IsNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
filter: graphqlFilter,
|
||||||
|
limit: DEFAULT_QUERY_PAGE_SIZE,
|
||||||
|
recordGqlFields: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDestroyClick = useCallback(async () => {
|
||||||
|
const recordsToDestroy = await fetchAllRecordIds();
|
||||||
|
const recordIdsToDestroy = recordsToDestroy.map((record) => record.id);
|
||||||
|
|
||||||
|
resetTableRowSelection();
|
||||||
|
|
||||||
|
await destroyManyRecords({ recordIdsToDestroy });
|
||||||
|
}, [destroyManyRecords, fetchAllRecordIds, resetTableRowSelection]);
|
||||||
|
|
||||||
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
|
const shouldBeRegistered =
|
||||||
|
!isRemoteObject &&
|
||||||
|
isDeletedFilterActive &&
|
||||||
|
isDefined(contextStoreNumberOfSelectedRecords) &&
|
||||||
|
contextStoreNumberOfSelectedRecords < BACKEND_BATCH_REQUEST_MAX_COUNT &&
|
||||||
|
contextStoreNumberOfSelectedRecords > 0;
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (!shouldBeRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDestroyRecordsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModal = (
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isDestroyRecordsModalOpen}
|
||||||
|
setIsOpen={setIsDestroyRecordsModalOpen}
|
||||||
|
title={'Permanently Destroy Records'}
|
||||||
|
subtitle={
|
||||||
|
"Are you sure you want to destroy these records? They won't be recoverable anymore."
|
||||||
|
}
|
||||||
|
onConfirmClick={handleDestroyClick}
|
||||||
|
deleteButtonText={'Destroy Records'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldBeRegistered,
|
||||||
|
onClick,
|
||||||
|
ConfirmationModal: confirmationModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export enum MultipleRecordsActionKeys {
|
export enum MultipleRecordsActionKeys {
|
||||||
DELETE = 'delete-multiple-records',
|
DELETE = 'delete-multiple-records',
|
||||||
EXPORT = 'export-multiple-records',
|
EXPORT = 'export-multiple-records',
|
||||||
|
DESTROY = 'destroy-multiple-records',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,9 @@ export type ActionHook =
|
|||||||
|
|
||||||
export type ActionHookWithoutObjectMetadataItem = () => ActionHookResult;
|
export type ActionHookWithoutObjectMetadataItem = () => ActionHookResult;
|
||||||
|
|
||||||
export type ActionHookWithObjectMetadataItem = ({
|
type ActionHookWithObjectMetadataItemParams = {
|
||||||
objectMetadataItem,
|
|
||||||
}: {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}) => ActionHookResult;
|
};
|
||||||
|
export type ActionHookWithObjectMetadataItem = (
|
||||||
|
params: ActionHookWithObjectMetadataItemParams,
|
||||||
|
) => ActionHookResult;
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const BACKEND_BATCH_REQUEST_MAX_COUNT = 10000;
|
||||||
@ -1 +0,0 @@
|
|||||||
export const DELETE_MAX_COUNT = 10000;
|
|
||||||
@ -19,7 +19,8 @@ type useDestroyManyRecordProps = {
|
|||||||
refetchFindManyQuery?: boolean;
|
refetchFindManyQuery?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DestroyManyRecordsOptions = {
|
export type DestroyManyRecordsProps = {
|
||||||
|
recordIdsToDestroy: string[];
|
||||||
skipOptimisticEffect?: boolean;
|
skipOptimisticEffect?: boolean;
|
||||||
delayInMsBetweenRequests?: number;
|
delayInMsBetweenRequests?: number;
|
||||||
};
|
};
|
||||||
@ -56,16 +57,19 @@ export const useDestroyManyRecords = ({
|
|||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
);
|
);
|
||||||
|
|
||||||
const destroyManyRecords = async (
|
const destroyManyRecords = async ({
|
||||||
idsToDestroy: string[],
|
recordIdsToDestroy,
|
||||||
options?: DestroyManyRecordsOptions,
|
delayInMsBetweenRequests,
|
||||||
) => {
|
skipOptimisticEffect = false,
|
||||||
const numberOfBatches = Math.ceil(idsToDestroy.length / mutationPageSize);
|
}: DestroyManyRecordsProps) => {
|
||||||
|
const numberOfBatches = Math.ceil(
|
||||||
|
recordIdsToDestroy.length / mutationPageSize,
|
||||||
|
);
|
||||||
|
|
||||||
const destroyedRecords = [];
|
const destroyedRecords = [];
|
||||||
|
|
||||||
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||||
const batchedIdToDestroy = idsToDestroy.slice(
|
const batchedIdToDestroy = recordIdsToDestroy.slice(
|
||||||
batchIndex * mutationPageSize,
|
batchIndex * mutationPageSize,
|
||||||
(batchIndex + 1) * mutationPageSize,
|
(batchIndex + 1) * mutationPageSize,
|
||||||
);
|
);
|
||||||
@ -80,7 +84,7 @@ export const useDestroyManyRecords = ({
|
|||||||
variables: {
|
variables: {
|
||||||
filter: { id: { in: batchedIdToDestroy } },
|
filter: { id: { in: batchedIdToDestroy } },
|
||||||
},
|
},
|
||||||
optimisticResponse: options?.skipOptimisticEffect
|
optimisticResponse: skipOptimisticEffect
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
[mutationResponseField]: batchedIdToDestroy.map(
|
[mutationResponseField]: batchedIdToDestroy.map(
|
||||||
@ -90,7 +94,7 @@ export const useDestroyManyRecords = ({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
update: options?.skipOptimisticEffect
|
update: skipOptimisticEffect
|
||||||
? undefined
|
? undefined
|
||||||
: (cache, { data }) => {
|
: (cache, { data }) => {
|
||||||
const records = data?.[mutationResponseField];
|
const records = data?.[mutationResponseField];
|
||||||
@ -126,8 +130,8 @@ export const useDestroyManyRecords = ({
|
|||||||
|
|
||||||
destroyedRecords.push(...destroyedRecordsForThisBatch);
|
destroyedRecords.push(...destroyedRecordsForThisBatch);
|
||||||
|
|
||||||
if (isDefined(options?.delayInMsBetweenRequests)) {
|
if (isDefined(delayInMsBetweenRequests)) {
|
||||||
await sleep(options.delayInMsBetweenRequests);
|
await sleep(delayInMsBetweenRequests);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,9 +87,12 @@ export const usePersistViewGroupRecords = () => {
|
|||||||
async (viewGroupsToDelete: ViewGroup[]) => {
|
async (viewGroupsToDelete: ViewGroup[]) => {
|
||||||
if (!viewGroupsToDelete.length) return;
|
if (!viewGroupsToDelete.length) return;
|
||||||
|
|
||||||
return destroyManyRecords(
|
const recordIdsToDestroy = viewGroupsToDelete.map(
|
||||||
viewGroupsToDelete.map((viewGroup) => viewGroup.id),
|
(viewGroup) => viewGroup.id,
|
||||||
);
|
);
|
||||||
|
return destroyManyRecords({
|
||||||
|
recordIdsToDestroy,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[destroyManyRecords],
|
[destroyManyRecords],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,14 +1,25 @@
|
|||||||
import { ReactNode, useEffect, useState } from 'react';
|
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
|
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import {
|
import {
|
||||||
ContextStoreTargetedRecordsRule,
|
ContextStoreTargetedRecordsRule,
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
|
||||||
|
export type JestContextStoreSetterMocks = {
|
||||||
|
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
|
||||||
|
contextStoreNumberOfSelectedRecords?: number;
|
||||||
|
contextStoreFilters?: RecordFilter[];
|
||||||
|
contextStoreCurrentObjectMetadataNameSingular?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type JestContextStoreSetterProps =
|
||||||
|
PropsWithChildren<JestContextStoreSetterMocks>;
|
||||||
export const JestContextStoreSetter = ({
|
export const JestContextStoreSetter = ({
|
||||||
contextStoreTargetedRecordsRule = {
|
contextStoreTargetedRecordsRule = {
|
||||||
mode: 'selection',
|
mode: 'selection',
|
||||||
@ -16,13 +27,9 @@ export const JestContextStoreSetter = ({
|
|||||||
},
|
},
|
||||||
contextStoreNumberOfSelectedRecords = 0,
|
contextStoreNumberOfSelectedRecords = 0,
|
||||||
contextStoreCurrentObjectMetadataNameSingular = '',
|
contextStoreCurrentObjectMetadataNameSingular = '',
|
||||||
|
contextStoreFilters = [],
|
||||||
children,
|
children,
|
||||||
}: {
|
}: JestContextStoreSetterProps) => {
|
||||||
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
|
|
||||||
contextStoreNumberOfSelectedRecords?: number;
|
|
||||||
contextStoreCurrentObjectMetadataNameSingular?: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}) => {
|
|
||||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
@ -35,6 +42,10 @@ export const JestContextStoreSetter = ({
|
|||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setcontextStoreFiltersComponentState = useSetRecoilComponentStateV2(
|
||||||
|
contextStoreFiltersComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
|
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
|
||||||
});
|
});
|
||||||
@ -46,6 +57,8 @@ export const JestContextStoreSetter = ({
|
|||||||
setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule);
|
setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule);
|
||||||
setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId);
|
setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId);
|
||||||
setContextStoreNumberOfSelectedRecords(contextStoreNumberOfSelectedRecords);
|
setContextStoreNumberOfSelectedRecords(contextStoreNumberOfSelectedRecords);
|
||||||
|
setcontextStoreFiltersComponentState(contextStoreFilters);
|
||||||
|
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
}, [
|
}, [
|
||||||
setContextStoreTargetedRecordsRule,
|
setContextStoreTargetedRecordsRule,
|
||||||
@ -54,6 +67,8 @@ export const JestContextStoreSetter = ({
|
|||||||
contextStoreCurrentObjectMetadataId,
|
contextStoreCurrentObjectMetadataId,
|
||||||
setContextStoreNumberOfSelectedRecords,
|
setContextStoreNumberOfSelectedRecords,
|
||||||
contextStoreNumberOfSelectedRecords,
|
contextStoreNumberOfSelectedRecords,
|
||||||
|
setcontextStoreFiltersComponentState,
|
||||||
|
contextStoreFilters,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return isLoaded ? <>{children}</> : null;
|
return isLoaded ? <>{children}</> : null;
|
||||||
|
|||||||
@ -1,23 +1,22 @@
|
|||||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||||
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
|
||||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||||
import { MockedResponse } from '@apollo/client/testing';
|
import { MockedResponse } from '@apollo/client/testing';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { MutableSnapshot } from 'recoil';
|
import { MutableSnapshot } from 'recoil';
|
||||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||||
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
|
import {
|
||||||
|
JestContextStoreSetter,
|
||||||
|
JestContextStoreSetterMocks,
|
||||||
|
} from '~/testing/jest/JestContextStoreSetter';
|
||||||
|
|
||||||
export type GetJestMetadataAndApolloMocksAndActionMenuWrapperProps = {
|
export type GetJestMetadataAndApolloMocksAndActionMenuWrapperProps = {
|
||||||
apolloMocks:
|
apolloMocks:
|
||||||
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
|
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
|
||||||
| undefined;
|
| undefined;
|
||||||
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
|
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
|
||||||
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
|
|
||||||
contextStoreNumberOfSelectedRecords?: number;
|
|
||||||
contextStoreCurrentObjectMetadataNameSingular?: string;
|
|
||||||
componentInstanceId: string;
|
componentInstanceId: string;
|
||||||
};
|
} & JestContextStoreSetterMocks;
|
||||||
|
|
||||||
export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
|
export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
|
||||||
apolloMocks,
|
apolloMocks,
|
||||||
@ -25,6 +24,7 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
|
|||||||
contextStoreTargetedRecordsRule,
|
contextStoreTargetedRecordsRule,
|
||||||
contextStoreNumberOfSelectedRecords,
|
contextStoreNumberOfSelectedRecords,
|
||||||
contextStoreCurrentObjectMetadataNameSingular,
|
contextStoreCurrentObjectMetadataNameSingular,
|
||||||
|
contextStoreFilters,
|
||||||
componentInstanceId,
|
componentInstanceId,
|
||||||
}: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps) => {
|
}: GetJestMetadataAndApolloMocksAndActionMenuWrapperProps) => {
|
||||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||||
@ -48,6 +48,7 @@ export const getJestMetadataAndApolloMocksAndActionMenuWrapper = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JestContextStoreSetter
|
<JestContextStoreSetter
|
||||||
|
contextStoreFilters={contextStoreFilters}
|
||||||
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
|
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
|
||||||
contextStoreNumberOfSelectedRecords={
|
contextStoreNumberOfSelectedRecords={
|
||||||
contextStoreNumberOfSelectedRecords
|
contextStoreNumberOfSelectedRecords
|
||||||
|
|||||||
Reference in New Issue
Block a user