Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

9018 fix batch delete #9149

Merged
merged 13 commits into from
Dec 20, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
Expand Down Expand Up @@ -40,9 +38,6 @@ export const useDeleteMultipleRecordsAction = ({
objectNameSingular: objectMetadataItem.nameSingular,
});

const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();

const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
Expand Down Expand Up @@ -76,26 +71,8 @@ export const useDeleteMultipleRecordsAction = ({

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,
});
}, [
deleteFavorite,
deleteManyRecords,
favorites,
fetchAllRecordIds,
resetTableRowSelection,
]);
await deleteManyRecords(recordIdsToDelete);
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);

const isRemoteObject = objectMetadataItem.isRemote;

Expand All @@ -105,7 +82,7 @@ export const useDeleteMultipleRecordsAction = ({
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;

const { isInRightDrawer, onActionExecutedCallback } =
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);

const registerDeleteMultipleRecordsAction = ({
Expand Down Expand Up @@ -133,9 +110,14 @@ export const useDeleteMultipleRecordsAction = ({
setIsOpen={setIsDeleteRecordsModalOpen}
title={'Delete Records'}
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
onConfirmClick={() => {
handleDeleteClick();
onActionExecutedCallback?.();
onConfirmClick={async () => {
onActionStartedCallback?.({
key: 'delete-multiple-records',
});
await handleDeleteClick();
onActionExecutedCallback?.({
key: 'delete-multiple-records',
});
if (isInRightDrawer) {
closeRightDrawer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada

const isRemoteObject = objectMetadataItem.isRemote;

const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
const { isInRightDrawer } = useContext(ActionMenuContext);

const shouldBeRegistered =
!isRemoteObject && isNull(selectedRecord?.deletedAt);
Expand All @@ -81,7 +80,6 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
}
onConfirmClick={() => {
handleDeleteClick();
onActionExecutedCallback?.();
if (isInRightDrawer) {
closeRightDrawer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetad
}
onConfirmClick={async () => {
await handleDeleteClick();
onActionExecutedCallback?.();
onActionExecutedCallback?.({ key: 'destroy-single-record' });
if (isInRightDrawer) {
closeRightDrawer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordInde
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';

import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useIsMobile } from 'twenty-ui';

export const RecordIndexActionMenu = () => {
export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
Expand All @@ -25,13 +27,27 @@ export const RecordIndexActionMenu = () => {

const isMobile = useIsMobile();

const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
isRecordIndexLoadMoreLockedComponentState,
indexId,
);

return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
onActionStartedCallback: (action) => {
if (action.key === 'delete-multiple-records') {
setIsLoadMoreLocked(true);
}
},
onActionExecutedCallback: (action) => {
if (action.key === 'delete-multiple-records') {
setIsLoadMoreLocked(false);
}
},
}}
>
{isPageHeaderV2Enabled ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const RecordIndexActionMenuButtons = () => {
variant="secondary"
accent="default"
title={entry.shortLabel}
onClick={() => entry.onClick?.()}
onClick={entry.onClick}
ariaLabel={entry.label}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { createContext } from 'react';

type ActionMenuContextType = {
isInRightDrawer: boolean;
onActionExecutedCallback: () => void;
onActionStartedCallback?: (action: { key: string }) => void;
onActionExecutedCallback?: (action: { key: string }) => void;
};

export const ActionMenuContext = createContext<ActionMenuContextType>({
isInRightDrawer: false,
onActionStartedCallback: () => {},
onActionExecutedCallback: () => {},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ApolloCache, StoreObject } from '@apollo/client';

import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import { isDefined } from '~/utils/isDefined';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';

// TODO: add extensive unit tests for this function
// That will also serve as documentation
export const triggerUpdateRecordOptimisticEffectByBatch = ({
cache,
objectMetadataItem,
currentRecords,
updatedRecords,
objectMetadataItems,
}: {
cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem;
currentRecords: RecordGqlNode[];
updatedRecords: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[];
}) => {
for (const [index, currentRecord] of currentRecords.entries()) {
triggerUpdateRelationsOptimisticEffect({
cache,
sourceObjectMetadataItem: objectMetadataItem,
currentSourceRecord: currentRecord,
updatedSourceRecord: updatedRecords[index],
objectMetadataItems,
});
}

cache.modify<StoreObject>({
fields: {
[objectMetadataItem.namePlural]: (
rootQueryCachedResponse,
{ readField, storeFieldName, toReference },
) => {
const shouldSkip = !isObjectRecordConnectionWithRefs(
objectMetadataItem.nameSingular,
rootQueryCachedResponse,
);

if (shouldSkip) {
return rootQueryCachedResponse;
}

const rootQueryConnection = rootQueryCachedResponse;

const { fieldVariables: rootQueryVariables } =
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
storeFieldName,
);

const rootQueryCurrentEdges =
readField<RecordGqlRefEdge[]>('edges', rootQueryConnection) ?? [];

let rootQueryNextEdges = [...rootQueryCurrentEdges];

const rootQueryFilter = rootQueryVariables?.filter;
const rootQueryOrderBy = rootQueryVariables?.orderBy;

for (const updatedRecord of updatedRecords) {
const updatedRecordMatchesThisRootQueryFilter =
isRecordMatchingFilter({
record: updatedRecord,
filter: rootQueryFilter ?? {},
objectMetadataItem,
});

const updatedRecordIndexInRootQueryEdges =
rootQueryCurrentEdges.findIndex(
(cachedEdge) =>
readField('id', cachedEdge.node) === updatedRecord.id,
);

const updatedRecordFoundInRootQueryEdges =
updatedRecordIndexInRootQueryEdges > -1;

const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
!updatedRecordFoundInRootQueryEdges;

const updatedRecordShouldBeRemovedFromRootQueryEdges =
!updatedRecordMatchesThisRootQueryFilter &&
updatedRecordFoundInRootQueryEdges;

if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord);

if (isDefined(updatedRecordNodeReference)) {
rootQueryNextEdges.push({
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
node: updatedRecordNodeReference,
cursor: '',
});
}
}

if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
}
}

const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);

if (
rootQueryNextEdgesShouldBeSorted &&
Object.getOwnPropertyNames(rootQueryOrderBy).length > 0
) {
rootQueryNextEdges = sortCachedObjectEdges({
edges: rootQueryNextEdges,
orderBy: rootQueryOrderBy,
readCacheField: readField,
});
}

return {
...rootQueryConnection,
edges: rootQueryNextEdges,
};
},
},
});
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useApolloClient } from '@apollo/client';

import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
Expand Down Expand Up @@ -80,6 +82,9 @@ export const useDeleteManyRecords = ({
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
.filter(isDefined);

const cachedRecordsWithConnection: RecordGqlNode[] = [];
const optimisticRecordsWithConnection: RecordGqlNode[] = [];

if (!options?.skipOptimisticEffect) {
cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord || !cachedRecord.id) {
Expand Down Expand Up @@ -112,20 +117,23 @@ export const useDeleteManyRecords = ({
return null;
}

cachedRecordsWithConnection.push(cachedRecordWithConnection);
optimisticRecordsWithConnection.push(optimisticRecordWithConnection);

updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: computedOptimisticRecord,
});
});

triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
triggerUpdateRecordOptimisticEffectByBatch({
cache: apolloClient.cache,
objectMetadataItem,
currentRecords: cachedRecordsWithConnection,
updatedRecords: optimisticRecordsWithConnection,
objectMetadataItems,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { GRAY_SCALE } from 'twenty-ui';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';

const StyledText = styled.div`
Expand All @@ -31,11 +33,23 @@ export const RecordBoardColumnFetchMoreLoader = () => {
columnDefinition.id,
);

const isLoadMoreLocked = useRecoilComponentValueV2(
isRecordIndexLoadMoreLockedComponentState,
);

const { ref, inView } = useInView();

useEffect(() => {
if (isLoadMoreLocked) {
return;
}

setShouldFetchMore(inView);
}, [setShouldFetchMore, inView]);
}, [setShouldFetchMore, inView, isLoadMoreLocked]);

if (isLoadMoreLocked) {
return null;
}

return (
<div ref={ref}>
Expand Down
Loading
Loading