mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +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 { 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';
|
||||
@ -40,9 +38,6 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
@ -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;
|
||||
|
||||
@ -105,7 +82,7 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
||||
contextStoreNumberOfSelectedRecords > 0;
|
||||
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const registerDeleteMultipleRecordsAction = ({
|
||||
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
@ -81,7 +80,6 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
||||
}
|
||||
onConfirmClick={() => {
|
||||
handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetad
|
||||
}
|
||||
onConfirmClick={async () => {
|
||||
await handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
onActionExecutedCallback?.({ key: 'destroy-single-record' });
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
@ -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 ? (
|
||||
|
@ -28,7 +28,7 @@ export const RecordIndexActionMenuButtons = () => {
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.shortLabel}
|
||||
onClick={() => entry.onClick?.()}
|
||||
onClick={entry.onClick}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
))}
|
||||
|
@ -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: () => {},
|
||||
});
|
||||
|
@ -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 { 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';
|
||||
@ -8,6 +9,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
|
||||
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';
|
||||
@ -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) {
|
||||
@ -112,21 +117,24 @@ export const useDeleteManyRecords = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
||||
optimisticRecordsWithConnection.push(optimisticRecordWithConnection);
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
triggerUpdateRecordOptimisticEffectByBatch({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
currentRecords: cachedRecordsWithConnection,
|
||||
updatedRecords: optimisticRecordsWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const deletedRecordsResponse = await apolloClient
|
||||
|
@ -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`
|
||||
@ -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}>
|
||||
|
@ -230,6 +230,7 @@ export const RecordIndexContainer = () => {
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</SpreadsheetImportProvider>
|
||||
<RecordIndexFiltersToContextStoreEffect />
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
@ -255,7 +256,9 @@ export const RecordIndexContainer = () => {
|
||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
|
||||
{!isPageHeaderV2Enabled && (
|
||||
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||
)}
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
</>
|
||||
|
@ -29,6 +29,8 @@ export const RecordIndexPageHeader = () => {
|
||||
|
||||
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
||||
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const numberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
@ -64,7 +66,7 @@ export const RecordIndexPageHeader = () => {
|
||||
|
||||
{isPageHeaderV2Enabled && (
|
||||
<>
|
||||
<RecordIndexActionMenu />
|
||||
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||
<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 { GRAY_SCALE } from 'twenty-ui';
|
||||
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
||||
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
@ -22,11 +23,19 @@ const StyledText = styled.div`
|
||||
export const RecordTableBodyFetchMoreLoader = () => {
|
||||
const { setRecordTableLastRowVisible } = useRecordTable();
|
||||
|
||||
const isRecordTableLoadMoreLocked = useRecoilComponentValueV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
);
|
||||
|
||||
const onLastRowVisible = useRecoilCallback(
|
||||
() => async (inView: boolean) => {
|
||||
if (isRecordTableLoadMoreLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRecordTableLastRowVisible(inView);
|
||||
},
|
||||
[setRecordTableLastRowVisible],
|
||||
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
|
||||
);
|
||||
|
||||
const scrollWrapperRef = useContext(
|
||||
@ -37,7 +46,8 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
||||
hasRecordTableFetchedAllRecordsComponentStateV2,
|
||||
);
|
||||
|
||||
const showLoadingMoreRow = !hasRecordTableFetchedAllRecordsComponents;
|
||||
const showLoadingMoreRow =
|
||||
!hasRecordTableFetchedAllRecordsComponents && !isRecordTableLoadMoreLocked;
|
||||
|
||||
const { ref: tbodyRef } = useInView({
|
||||
onChange: onLastRowVisible,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
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 { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
@ -20,6 +21,10 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
||||
currentRecordGroupId,
|
||||
);
|
||||
|
||||
const isLoadMoreLocked = useRecoilComponentValueV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
);
|
||||
|
||||
const recordIds = useRecoilComponentValueV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
@ -28,7 +33,7 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
||||
fetchMoreRecords();
|
||||
};
|
||||
|
||||
if (hasFetchedAllRecords) {
|
||||
if (hasFetchedAllRecords || isLoadMoreLocked) {
|
||||
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 { CalendarModule } from 'src/modules/calendar/calendar.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 { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||
@ -50,6 +51,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
TimelineJobModule,
|
||||
WebhookJobModule,
|
||||
WorkflowModule,
|
||||
FavoriteModule,
|
||||
],
|
||||
providers: [
|
||||
CleanInactiveWorkspaceJob,
|
||||
|
@ -18,4 +18,5 @@ export enum MessageQueue {
|
||||
testQueue = 'test-queue',
|
||||
workflowQueue = 'workflow-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 { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.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 { ViewModule } from 'src/modules/view/view.module';
|
||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
@ -15,6 +16,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
ViewModule,
|
||||
WorkflowModule,
|
||||
FavoriteFolderModule,
|
||||
FavoriteModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
|
Loading…
Reference in New Issue
Block a user