mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
parent
a0b5720831
commit
925294675c
@ -8,8 +8,6 @@ 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 { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
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 { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||||
@ -40,9 +38,6 @@ export const useDeleteMultipleRecordsAction = ({
|
|||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sortedFavorites: favorites } = useFavorites();
|
|
||||||
const { deleteFavorite } = useDeleteFavorite();
|
|
||||||
|
|
||||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
);
|
);
|
||||||
@ -76,26 +71,8 @@ export const useDeleteMultipleRecordsAction = ({
|
|||||||
|
|
||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
|
|
||||||
for (const recordIdToDelete of recordIdsToDelete) {
|
await deleteManyRecords(recordIdsToDelete);
|
||||||
const foundFavorite = favorites?.find(
|
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
|
||||||
(favorite) => favorite.recordId === recordIdToDelete,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (foundFavorite !== undefined) {
|
|
||||||
deleteFavorite(foundFavorite.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteManyRecords(recordIdsToDelete, {
|
|
||||||
delayInMsBetweenRequests: 50,
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
deleteFavorite,
|
|
||||||
deleteManyRecords,
|
|
||||||
favorites,
|
|
||||||
fetchAllRecordIds,
|
|
||||||
resetTableRowSelection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isRemoteObject = objectMetadataItem.isRemote;
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
@ -105,7 +82,7 @@ export const useDeleteMultipleRecordsAction = ({
|
|||||||
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
||||||
contextStoreNumberOfSelectedRecords > 0;
|
contextStoreNumberOfSelectedRecords > 0;
|
||||||
|
|
||||||
const { isInRightDrawer, onActionExecutedCallback } =
|
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
|
||||||
useContext(ActionMenuContext);
|
useContext(ActionMenuContext);
|
||||||
|
|
||||||
const registerDeleteMultipleRecordsAction = ({
|
const registerDeleteMultipleRecordsAction = ({
|
||||||
@ -133,9 +110,14 @@ export const useDeleteMultipleRecordsAction = ({
|
|||||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||||
title={'Delete Records'}
|
title={'Delete Records'}
|
||||||
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
|
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
|
||||||
onConfirmClick={() => {
|
onConfirmClick={async () => {
|
||||||
handleDeleteClick();
|
onActionStartedCallback?.({
|
||||||
onActionExecutedCallback?.();
|
key: 'delete-multiple-records',
|
||||||
|
});
|
||||||
|
await handleDeleteClick();
|
||||||
|
onActionExecutedCallback?.({
|
||||||
|
key: 'delete-multiple-records',
|
||||||
|
});
|
||||||
if (isInRightDrawer) {
|
if (isInRightDrawer) {
|
||||||
closeRightDrawer();
|
closeRightDrawer();
|
||||||
}
|
}
|
||||||
|
@ -54,8 +54,7 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
|||||||
|
|
||||||
const isRemoteObject = objectMetadataItem.isRemote;
|
const isRemoteObject = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
const { isInRightDrawer, onActionExecutedCallback } =
|
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||||
useContext(ActionMenuContext);
|
|
||||||
|
|
||||||
const shouldBeRegistered =
|
const shouldBeRegistered =
|
||||||
!isRemoteObject && isNull(selectedRecord?.deletedAt);
|
!isRemoteObject && isNull(selectedRecord?.deletedAt);
|
||||||
@ -81,7 +80,6 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
|||||||
}
|
}
|
||||||
onConfirmClick={() => {
|
onConfirmClick={() => {
|
||||||
handleDeleteClick();
|
handleDeleteClick();
|
||||||
onActionExecutedCallback?.();
|
|
||||||
if (isInRightDrawer) {
|
if (isInRightDrawer) {
|
||||||
closeRightDrawer();
|
closeRightDrawer();
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetad
|
|||||||
}
|
}
|
||||||
onConfirmClick={async () => {
|
onConfirmClick={async () => {
|
||||||
await handleDeleteClick();
|
await handleDeleteClick();
|
||||||
onActionExecutedCallback?.();
|
onActionExecutedCallback?.({ key: 'destroy-single-record' });
|
||||||
if (isInRightDrawer) {
|
if (isInRightDrawer) {
|
||||||
closeRightDrawer();
|
closeRightDrawer();
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@ import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordInde
|
|||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
|
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
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 { 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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useIsMobile } from 'twenty-ui';
|
import { useIsMobile } from 'twenty-ui';
|
||||||
|
|
||||||
export const RecordIndexActionMenu = () => {
|
export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
|
||||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||||
contextStoreCurrentObjectMetadataIdComponentState,
|
contextStoreCurrentObjectMetadataIdComponentState,
|
||||||
);
|
);
|
||||||
@ -25,13 +27,27 @@ export const RecordIndexActionMenu = () => {
|
|||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
|
||||||
|
isRecordIndexLoadMoreLockedComponentState,
|
||||||
|
indexId,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextStoreCurrentObjectMetadataId && (
|
{contextStoreCurrentObjectMetadataId && (
|
||||||
<ActionMenuContext.Provider
|
<ActionMenuContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isInRightDrawer: false,
|
isInRightDrawer: false,
|
||||||
onActionExecutedCallback: () => {},
|
onActionStartedCallback: (action) => {
|
||||||
|
if (action.key === 'delete-multiple-records') {
|
||||||
|
setIsLoadMoreLocked(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onActionExecutedCallback: (action) => {
|
||||||
|
if (action.key === 'delete-multiple-records') {
|
||||||
|
setIsLoadMoreLocked(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPageHeaderV2Enabled ? (
|
{isPageHeaderV2Enabled ? (
|
||||||
|
@ -28,7 +28,7 @@ export const RecordIndexActionMenuButtons = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
accent="default"
|
accent="default"
|
||||||
title={entry.shortLabel}
|
title={entry.shortLabel}
|
||||||
onClick={() => entry.onClick?.()}
|
onClick={entry.onClick}
|
||||||
ariaLabel={entry.label}
|
ariaLabel={entry.label}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -2,10 +2,12 @@ import { createContext } from 'react';
|
|||||||
|
|
||||||
type ActionMenuContextType = {
|
type ActionMenuContextType = {
|
||||||
isInRightDrawer: boolean;
|
isInRightDrawer: boolean;
|
||||||
onActionExecutedCallback: () => void;
|
onActionStartedCallback?: (action: { key: string }) => void;
|
||||||
|
onActionExecutedCallback?: (action: { key: string }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActionMenuContext = createContext<ActionMenuContextType>({
|
export const ActionMenuContext = createContext<ActionMenuContextType>({
|
||||||
isInRightDrawer: false,
|
isInRightDrawer: false,
|
||||||
|
onActionStartedCallback: () => {},
|
||||||
onActionExecutedCallback: () => {},
|
onActionExecutedCallback: () => {},
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||||
|
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
|
||||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
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';
|
||||||
@ -8,6 +9,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
|
|||||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
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 { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
|
||||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
@ -80,6 +82,9 @@ export const useDeleteManyRecords = ({
|
|||||||
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
|
const cachedRecordsWithConnection: RecordGqlNode[] = [];
|
||||||
|
const optimisticRecordsWithConnection: RecordGqlNode[] = [];
|
||||||
|
|
||||||
if (!options?.skipOptimisticEffect) {
|
if (!options?.skipOptimisticEffect) {
|
||||||
cachedRecords.forEach((cachedRecord) => {
|
cachedRecords.forEach((cachedRecord) => {
|
||||||
if (!cachedRecord || !cachedRecord.id) {
|
if (!cachedRecord || !cachedRecord.id) {
|
||||||
@ -112,20 +117,23 @@ export const useDeleteManyRecords = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
||||||
|
optimisticRecordsWithConnection.push(optimisticRecordWithConnection);
|
||||||
|
|
||||||
updateRecordFromCache({
|
updateRecordFromCache({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
record: computedOptimisticRecord,
|
record: computedOptimisticRecord,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffectByBatch({
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
currentRecord: cachedRecordWithConnection,
|
currentRecords: cachedRecordsWithConnection,
|
||||||
updatedRecord: optimisticRecordWithConnection,
|
updatedRecords: optimisticRecordsWithConnection,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import { GRAY_SCALE } from 'twenty-ui';
|
|||||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
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';
|
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||||
|
|
||||||
const StyledText = styled.div`
|
const StyledText = styled.div`
|
||||||
@ -31,11 +33,23 @@ export const RecordBoardColumnFetchMoreLoader = () => {
|
|||||||
columnDefinition.id,
|
columnDefinition.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoadMoreLocked = useRecoilComponentValueV2(
|
||||||
|
isRecordIndexLoadMoreLockedComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { ref, inView } = useInView();
|
const { ref, inView } = useInView();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isLoadMoreLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setShouldFetchMore(inView);
|
setShouldFetchMore(inView);
|
||||||
}, [setShouldFetchMore, inView]);
|
}, [setShouldFetchMore, inView, isLoadMoreLocked]);
|
||||||
|
|
||||||
|
if (isLoadMoreLocked) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
|
@ -230,6 +230,7 @@ export const RecordIndexContainer = () => {
|
|||||||
objectNamePlural={objectNamePlural}
|
objectNamePlural={objectNamePlural}
|
||||||
viewBarId={recordIndexId}
|
viewBarId={recordIndexId}
|
||||||
/>
|
/>
|
||||||
|
<RecordIndexTableContainerEffect />
|
||||||
</SpreadsheetImportProvider>
|
</SpreadsheetImportProvider>
|
||||||
<RecordIndexFiltersToContextStoreEffect />
|
<RecordIndexFiltersToContextStoreEffect />
|
||||||
{recordIndexViewType === ViewType.Table && (
|
{recordIndexViewType === ViewType.Table && (
|
||||||
@ -255,7 +256,9 @@ export const RecordIndexContainer = () => {
|
|||||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||||
</StyledContainerWithPadding>
|
</StyledContainerWithPadding>
|
||||||
)}
|
)}
|
||||||
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
|
{!isPageHeaderV2Enabled && (
|
||||||
|
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||||
|
)}
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</>
|
</>
|
||||||
|
@ -29,6 +29,8 @@ export const RecordIndexPageHeader = () => {
|
|||||||
|
|
||||||
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
||||||
|
|
||||||
|
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
const numberOfSelectedRecords = useRecoilComponentValueV2(
|
const numberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
);
|
);
|
||||||
@ -64,7 +66,7 @@ export const RecordIndexPageHeader = () => {
|
|||||||
|
|
||||||
{isPageHeaderV2Enabled && (
|
{isPageHeaderV2Enabled && (
|
||||||
<>
|
<>
|
||||||
<RecordIndexActionMenu />
|
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||||
<PageHeaderOpenCommandMenuButton />
|
<PageHeaderOpenCommandMenuButton />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||||
|
|
||||||
|
export const isRecordIndexLoadMoreLockedComponentState =
|
||||||
|
createComponentStateV2<boolean>({
|
||||||
|
key: 'isRecordIndexLoadMoreLockedComponentState',
|
||||||
|
componentInstanceContext: ViewComponentInstanceContext,
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
@ -4,6 +4,7 @@ import { useInView } from 'react-intersection-observer';
|
|||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { GRAY_SCALE } from 'twenty-ui';
|
import { GRAY_SCALE } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
||||||
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||||
@ -22,11 +23,19 @@ const StyledText = styled.div`
|
|||||||
export const RecordTableBodyFetchMoreLoader = () => {
|
export const RecordTableBodyFetchMoreLoader = () => {
|
||||||
const { setRecordTableLastRowVisible } = useRecordTable();
|
const { setRecordTableLastRowVisible } = useRecordTable();
|
||||||
|
|
||||||
|
const isRecordTableLoadMoreLocked = useRecoilComponentValueV2(
|
||||||
|
isRecordIndexLoadMoreLockedComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const onLastRowVisible = useRecoilCallback(
|
const onLastRowVisible = useRecoilCallback(
|
||||||
() => async (inView: boolean) => {
|
() => async (inView: boolean) => {
|
||||||
|
if (isRecordTableLoadMoreLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setRecordTableLastRowVisible(inView);
|
setRecordTableLastRowVisible(inView);
|
||||||
},
|
},
|
||||||
[setRecordTableLastRowVisible],
|
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scrollWrapperRef = useContext(
|
const scrollWrapperRef = useContext(
|
||||||
@ -37,7 +46,8 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
|||||||
hasRecordTableFetchedAllRecordsComponentStateV2,
|
hasRecordTableFetchedAllRecordsComponentStateV2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const showLoadingMoreRow = !hasRecordTableFetchedAllRecordsComponents;
|
const showLoadingMoreRow =
|
||||||
|
!hasRecordTableFetchedAllRecordsComponents && !isRecordTableLoadMoreLocked;
|
||||||
|
|
||||||
const { ref: tbodyRef } = useInView({
|
const { ref: tbodyRef } = useInView({
|
||||||
onChange: onLastRowVisible,
|
onChange: onLastRowVisible,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||||
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
|
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
|
||||||
|
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||||
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
|
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
|
||||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||||
@ -20,6 +21,10 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
|||||||
currentRecordGroupId,
|
currentRecordGroupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoadMoreLocked = useRecoilComponentValueV2(
|
||||||
|
isRecordIndexLoadMoreLockedComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const recordIds = useRecoilComponentValueV2(
|
const recordIds = useRecoilComponentValueV2(
|
||||||
recordIndexAllRecordIdsComponentSelector,
|
recordIndexAllRecordIdsComponentSelector,
|
||||||
);
|
);
|
||||||
@ -28,7 +33,7 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
|||||||
fetchMoreRecords();
|
fetchMoreRecords();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasFetchedAllRecords) {
|
if (hasFetchedAllRecords || isLoadMoreLocked) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspac
|
|||||||
import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
|
import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
|
||||||
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||||
import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-creation-manager/jobs/auto-companies-and-contacts-creation-job.module';
|
import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-creation-manager/jobs/auto-companies-and-contacts-creation-job.module';
|
||||||
|
import { FavoriteModule } from 'src/modules/favorite/favorite.module';
|
||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
||||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||||
@ -50,6 +51,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
|||||||
TimelineJobModule,
|
TimelineJobModule,
|
||||||
WebhookJobModule,
|
WebhookJobModule,
|
||||||
WorkflowModule,
|
WorkflowModule,
|
||||||
|
FavoriteModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CleanInactiveWorkspaceJob,
|
CleanInactiveWorkspaceJob,
|
||||||
|
@ -18,4 +18,5 @@ export enum MessageQueue {
|
|||||||
testQueue = 'test-queue',
|
testQueue = 'test-queue',
|
||||||
workflowQueue = 'workflow-queue',
|
workflowQueue = 'workflow-queue',
|
||||||
serverlessFunctionQueue = 'serverless-function-queue',
|
serverlessFunctionQueue = 'serverless-function-queue',
|
||||||
|
favoriteQueue = 'favorite-queue',
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export const FAVORITE_DELETION_BATCH_SIZE = 100;
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { FavoriteDeletionJob } from 'src/modules/favorite/jobs/favorite-deletion.job';
|
||||||
|
import { FavoriteDeletionListener } from 'src/modules/favorite/listeners/favorite-deletion.listener';
|
||||||
|
import { FavoriteDeletionService } from 'src/modules/favorite/services/favorite-deletion.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature(
|
||||||
|
[ObjectMetadataEntity, FieldMetadataEntity],
|
||||||
|
'metadata',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
FavoriteDeletionService,
|
||||||
|
FavoriteDeletionListener,
|
||||||
|
FavoriteDeletionJob,
|
||||||
|
],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class FavoriteModule {}
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Scope } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||||
|
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||||
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
|
import { FavoriteDeletionService } from 'src/modules/favorite/services/favorite-deletion.service';
|
||||||
|
|
||||||
|
export type FavoriteDeletionJobData = {
|
||||||
|
workspaceId: string;
|
||||||
|
deletedRecordIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Processor({
|
||||||
|
queueName: MessageQueue.favoriteQueue,
|
||||||
|
scope: Scope.REQUEST,
|
||||||
|
})
|
||||||
|
export class FavoriteDeletionJob {
|
||||||
|
constructor(
|
||||||
|
private readonly favoriteDeletionService: FavoriteDeletionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Process(FavoriteDeletionJob.name)
|
||||||
|
async handle(data: FavoriteDeletionJobData): Promise<void> {
|
||||||
|
await this.favoriteDeletionService.deleteFavoritesForDeletedRecords(
|
||||||
|
data.deletedRecordIds,
|
||||||
|
data.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
|
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import {
|
||||||
|
FavoriteDeletionJob,
|
||||||
|
FavoriteDeletionJobData,
|
||||||
|
} from 'src/modules/favorite/jobs/favorite-deletion.job';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FavoriteDeletionListener {
|
||||||
|
constructor(
|
||||||
|
@InjectMessageQueue(MessageQueue.favoriteQueue)
|
||||||
|
private readonly messageQueueService: MessageQueueService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('*', DatabaseEventAction.DELETED)
|
||||||
|
async handleDeletedEvent(
|
||||||
|
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent>,
|
||||||
|
) {
|
||||||
|
const deletedRecordIds = payload.events.map(({ recordId }) => recordId);
|
||||||
|
|
||||||
|
await this.messageQueueService.add<FavoriteDeletionJobData>(
|
||||||
|
FavoriteDeletionJob.name,
|
||||||
|
{
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
deletedRecordIds,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { In, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FieldMetadataEntity,
|
||||||
|
FieldMetadataType,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { FAVORITE_DELETION_BATCH_SIZE } from 'src/modules/favorite/constants/favorite-deletion-batch-size';
|
||||||
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FavoriteDeletionService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
|
||||||
|
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||||
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||||
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async deleteFavoritesForDeletedRecords(
|
||||||
|
deletedRecordIds: string[],
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const favoriteRepository =
|
||||||
|
await this.twentyORMManager.getRepository<FavoriteWorkspaceEntity>(
|
||||||
|
'favorite',
|
||||||
|
);
|
||||||
|
|
||||||
|
const favoriteObjectMetadata = await this.objectMetadataRepository.findOne({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'favorite',
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!favoriteObjectMetadata) {
|
||||||
|
throw new Error('Favorite object metadata not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const favoriteFields = await this.fieldMetadataRepository.find({
|
||||||
|
where: {
|
||||||
|
objectMetadataId: favoriteObjectMetadata.id,
|
||||||
|
type: FieldMetadataType.RELATION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const favoritesToDelete = await favoriteRepository.find({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: favoriteFields.map((field) => ({
|
||||||
|
[`${field.name}Id`]: In(deletedRecordIds),
|
||||||
|
})),
|
||||||
|
withDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (favoritesToDelete.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const favoriteIdsToDelete = favoritesToDelete.map(
|
||||||
|
(favorite) => favorite.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const batches: string[][] = [];
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < favoriteIdsToDelete.length;
|
||||||
|
i += FAVORITE_DELETION_BATCH_SIZE
|
||||||
|
) {
|
||||||
|
batches.push(
|
||||||
|
favoriteIdsToDelete.slice(i, i + FAVORITE_DELETION_BATCH_SIZE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
await favoriteRepository.delete(batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
import { FavoriteFolderModule } from 'src/modules/favorite-folder/favorite-folder.module';
|
import { FavoriteFolderModule } from 'src/modules/favorite-folder/favorite-folder.module';
|
||||||
|
import { FavoriteModule } from 'src/modules/favorite/favorite.module';
|
||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
import { ViewModule } from 'src/modules/view/view.module';
|
import { ViewModule } from 'src/modules/view/view.module';
|
||||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||||
@ -15,6 +16,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
|||||||
ViewModule,
|
ViewModule,
|
||||||
WorkflowModule,
|
WorkflowModule,
|
||||||
FavoriteFolderModule,
|
FavoriteFolderModule,
|
||||||
|
FavoriteModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
Loading…
Reference in New Issue
Block a user