Fix fetch more notes (#16442)

fixes : https://github.com/twentyhq/twenty/issues/16320

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2025-12-12 14:43:21 +01:00
committed by GitHub
parent 8dbcd506ed
commit 6acdde72ef
13 changed files with 607 additions and 114 deletions
@@ -15,7 +15,7 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
}));
const mockActivityTarget = {
__typename: 'ActivityTarget',
__typename: 'TaskTarget',
updatedAt: '2021-08-03T19:20:06.000Z',
createdAt: '2021-08-03T19:20:06.000Z',
personId: '1',
@@ -55,18 +55,11 @@ describe('useActivities', () => {
);
useActivityTargetsForTargetableObjectsMock.useActivityTargetsForTargetableObjects.mockReturnValue(
{
activityTargets: [mockActivityTarget],
activityTargets: [{ ...mockActivityTarget, task: mockActivity }],
loadingActivityTargets: false,
},
);
const useFindManyRecordsMock = jest.requireMock(
'@/object-record/hooks/useFindManyRecords',
);
useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
records: [mockActivity],
});
const { result } = renderHook(
() => {
const activities = useActivities({
@@ -74,9 +67,9 @@ describe('useActivities', () => {
targetableObjects: [
{ targetObjectNameSingular: 'company', id: '123' },
],
activitiesFilters: {},
activitiesOrderByVariables: [{}],
skip: false,
limit: 10,
activityTargetsOrderByVariables: [{}],
});
return activities;
},
@@ -1,92 +1,84 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { type Note } from '@/activities/types/Note';
import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type Task } from '@/activities/types/Task';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type RecordGqlOperationFilter } from 'twenty-shared/types';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useActivities = <T extends Task | Note>({
objectNameSingular,
targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
activityTargetsOrderByVariables,
skip,
limit,
}: {
objectNameSingular: CoreObjectNameSingular;
objectNameSingular: CoreObjectNameSingular.Note | CoreObjectNameSingular.Task;
targetableObjects: ActivityTargetableObject[];
activitiesFilters: RecordGqlOperationFilter;
activitiesOrderByVariables: RecordGqlOperationOrderBy;
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
skip?: boolean;
limit: number;
}) => {
const { objectMetadataItems } = useObjectMetadataItems();
const updateActivitiesInStore = useRecoilCallback(
({ set }) =>
(activityTargets: (TaskTarget | NoteTarget)[]) => {
for (const activityTarget of activityTargets) {
const activity = activityTarget[objectNameSingular];
set(recordStoreFamilyState(activity.id), activity);
}
},
[objectNameSingular],
);
const { activityTargets, loadingActivityTargets } =
useActivityTargetsForTargetableObjects({
objectNameSingular,
targetableObjects,
skip: skip,
const {
activityTargets,
loadingActivityTargets,
totalCountActivityTargets,
fetchMoreActivityTargets,
hasNextPage,
} = useActivityTargetsForTargetableObjects({
objectNameSingular,
targetableObjects,
skip: skip,
activityTargetsOrderByVariables,
onCompleted: updateActivitiesInStore,
limit,
});
const activities = activityTargets.map((activityTarget) => {
return activityTarget[objectNameSingular];
}) as T[];
const fetchMoreActivities = async () => {
const result = await fetchMoreActivityTargets();
if (!isDefined(result?.data)) {
return [];
}
const activityTargets = getRecordsFromRecordConnection<
TaskTarget | NoteTarget
>({
recordConnection: result.data,
});
const activityIds = [
...new Set(
activityTargets
? [
...activityTargets
.map(
(activityTarget) =>
activityTarget.taskId ?? activityTarget.noteId,
)
.filter(isNonEmptyString),
].sort(sortByAscString)
: [],
),
];
updateActivitiesInStore(activityTargets);
const skipBecauseNoActivityTargetFound = activityIds.length === 0;
const filter: RecordGqlOperationFilter = {
id: {
in: activityIds,
},
...activitiesFilters,
return activityTargets.map((activityTarget) => {
return activityTarget[objectNameSingular];
}) as T[];
};
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
findActivitiesOperationSignatureFactory({
objectMetadataItems,
objectNameSingular,
});
const { records: activities, loading: loadingActivities } =
useFindManyRecords<Task | Note>({
skip: skip || loadingActivityTargets || skipBecauseNoActivityTargetFound,
objectNameSingular:
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
recordGqlFields: FIND_ACTIVITIES_OPERATION_SIGNATURE.fields,
filter,
orderBy: activitiesOrderByVariables,
onCompleted: useRecoilCallback(
({ set }) =>
(activities) => {
for (const activity of activities) {
set(recordStoreFamilyState(activity.id), activity);
}
},
[],
),
});
return {
activities: activities as T[],
loading: loadingActivities || loadingActivityTargets,
loading: loadingActivityTargets,
totalCountActivities: totalCountActivityTargets,
fetchMoreActivities,
hasNextPage,
};
};
@@ -7,6 +7,7 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargetsForTargetableObjects = ({
@@ -14,14 +15,18 @@ export const useActivityTargetsForTargetableObjects = ({
targetableObjects,
skip,
onCompleted,
activityTargetsOrderByVariables,
limit,
}: {
objectNameSingular: CoreObjectNameSingular;
objectNameSingular: CoreObjectNameSingular.Note | CoreObjectNameSingular.Task;
targetableObjects: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>[];
skip?: boolean;
onCompleted?: (activityTargets: (TaskTarget | NoteTarget)[]) => void;
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
limit: number;
}) => {
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
@@ -38,18 +43,28 @@ export const useActivityTargetsForTargetableObjects = ({
// TODO: We want to optimistically remove from this request
// If we are on a show page and we remove the current show page object corresponding activity target
// See also if we need to update useTimelineActivities
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords<TaskTarget | NoteTarget>({
skip,
objectNameSingular:
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,
filter: activityTargetsFilter,
recordGqlFields: FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.fields,
onCompleted,
});
const {
records: activityTargets,
loading: loadingActivityTargets,
totalCount: totalCountActivityTargets,
fetchMoreRecords: fetchMoreActivityTargets,
hasNextPage,
} = useFindManyRecords<TaskTarget | NoteTarget>({
skip,
objectNameSingular:
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,
filter: activityTargetsFilter,
recordGqlFields: FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.fields,
onCompleted,
orderBy: activityTargetsOrderByVariables,
limit,
});
return {
activityTargets,
loadingActivityTargets,
totalCountActivityTargets: totalCountActivityTargets ?? 0,
fetchMoreActivityTargets,
hasNextPage,
};
};