diff --git a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx index e451f57ed..d61ebc25b 100644 --- a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx +++ b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx @@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil'; import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { avatarUrl } from '~/testing/mock-data/users'; import { CommentHeader } from '../CommentHeader'; @@ -32,7 +32,7 @@ const meta: Meta = { ), - ComponentDecorator, + ComponentWithRouterDecorator, ], argTypes: { actionBar: { diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx new file mode 100644 index 000000000..a43b53fba --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx @@ -0,0 +1,119 @@ +import { renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; + +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; +import { Comment } from '@/activities/types/Comment'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; + +const mockActivityWithConnectionRelation = { + activityTargets: { + edges: [ + { + __typename: 'ActivityTargetEdge', + node: { + id: '20202020-1029-4661-9e91-83bad932bdff', + }, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }, + comments: { + edges: [ + { + __typename: 'CommentEdge', + node: { + id: '20202020-1029-4661-9e91-83bad932bdee', + }, + }, + ] as ObjectRecordEdge[], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }, +}; + +const mockActivityWithArrayRelation = { + activityTargets: [ + { + id: '20202020-1029-4661-9e91-83bad932bdff', + }, + ], + comments: [ + { + id: '20202020-1029-4661-9e91-83bad932bdee', + }, + ], +}; + +describe('useActivityConnectionUtils', () => { + it('Should turn activity with connection relation in activity with array relation', async () => { + const { result } = renderHook(() => useActivityConnectionUtils(), { + wrapper: ({ children }) => ( + { + snapshot.set( + objectMetadataItemsState, + getObjectMetadataItemsMock(), + ); + }} + > + {children} + + ), + }); + + const { makeActivityWithoutConnection } = result.current; + + const { activity: activityWithArrayRelation } = + makeActivityWithoutConnection(mockActivityWithConnectionRelation as any); + + expect(activityWithArrayRelation).toBeDefined(); + + expect(activityWithArrayRelation.activityTargets[0].id).toEqual( + mockActivityWithArrayRelation.activityTargets[0].id, + ); + }); + + it('Should turn activity with connection relation in activity with array relation', async () => { + const { result } = renderHook(() => useActivityConnectionUtils(), { + wrapper: ({ children }) => ( + { + snapshot.set( + objectMetadataItemsState, + getObjectMetadataItemsMock(), + ); + }} + > + {children} + + ), + }); + + const { makeActivityWithConnection } = result.current; + + const { activityWithConnection } = makeActivityWithConnection( + mockActivityWithArrayRelation as any, + ); + + expect(activityWithConnection).toBeDefined(); + + console.log( + JSON.stringify({ + mockActivityWithConnectionRelation, + activityWithConnection, + mockActivityWithArrayRelation, + }), + ); + + expect(activityWithConnection.activityTargets.edges[0].node.id).toEqual( + mockActivityWithConnectionRelation.activityTargets.edges[0].node.id, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts index 804426cab..ecbc37f3f 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivities.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivities.ts @@ -2,10 +2,10 @@ import { useEffect, useState } from 'react'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { OrderByField } from '@/object-metadata/types/OrderByField'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts index 547d902dc..4f63b2d1b 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityById.ts @@ -1,4 +1,4 @@ -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; diff --git a/packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/utils/useActivityConnectionUtils.ts rename to packages/twenty-front/src/modules/activities/hooks/useActivityConnectionUtils.ts diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts index ed6e7a556..5ade36582 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInDB.ts @@ -1,9 +1,8 @@ import { isNonEmptyArray } from '@sniptt/guards'; -import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; @@ -20,9 +19,6 @@ export const useCreateActivityInDB = () => { const { makeActivityWithConnection } = useActivityConnectionUtils(); - const { modifyActivityTargetsOnActivityCache } = - useModifyActivityTargetsOnActivityCache(); - const createActivityInDB = async (activityToCreate: ActivityForEditor) => { const { activityWithConnection } = makeActivityWithConnection( activityToCreate as any, // TODO: fix type @@ -45,12 +41,6 @@ export const useCreateActivityInDB = () => { skipOptimisticEffect: true, }); } - - // TODO: replace by trigger optimistic effect - modifyActivityTargetsOnActivityCache({ - activityId: activityToCreate.id, - activityTargets: activityTargetsToCreate, - }); }; return { diff --git a/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts b/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts index 26db54c7c..f4cd5914b 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useDeleteActivityFromCache.ts @@ -1,7 +1,7 @@ import { useApolloClient } from '@apollo/client'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { ActivityForEditor } from '@/activities/types/ActivityForEditor'; -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts new file mode 100644 index 000000000..e6d09e329 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQueries.ts @@ -0,0 +1,175 @@ +import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; + +import { Activity } from '@/activities/types/Activity'; +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { OrderByField } from '@/object-metadata/types/OrderByField'; +import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortByAscString } from '~/utils/array/sortByAscString'; + +// TODO: create a generic hook from this +export const useInjectIntoActivitiesQueries = () => { + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const { + upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivity, + }); + + const injectActivitiesQueries = ({ + activityToInject, + activityTargetsToInject, + targetableObjects, + activitiesFilters, + activitiesOrderByVariables, + injectOnlyInIdFilter, + }: { + activityToInject: Activity; + activityTargetsToInject: ActivityTarget[]; + targetableObjects: ActivityTargetableObject[]; + activitiesFilters?: ObjectRecordQueryFilter; + activitiesOrderByVariables?: OrderByField; + injectOnlyInIdFilter?: boolean; + }) => { + const hasActivityTargets = isNonEmptyArray(targetableObjects); + + if (hasActivityTargets) { + const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ + targetableObjects, + }); + + const findManyActivitiyTargetsQueryVariables = { + filter: findManyActivitiyTargetsQueryFilter, + }; + + const existingActivityTargetsWithMaybeDuplicates = + readFindManyActivityTargetsQueryInCache({ + queryVariables: findManyActivitiyTargetsQueryVariables, + }); + + const existingActivityTargetsWithoutDuplicates: ObjectRecord[] = + existingActivityTargetsWithMaybeDuplicates.filter( + (existingActivityTarget) => + !activityTargetsToInject.some( + (activityTargetToInject) => + activityTargetToInject.id === existingActivityTarget.id, + ), + ); + + const existingActivityIdsFromTargets = + existingActivityTargetsWithoutDuplicates + ?.map((activityTarget) => activityTarget.activityId) + .filter(isNonEmptyString); + + const currentFindManyActivitiesQueryVariables = { + filter: { + id: { + in: existingActivityIdsFromTargets.toSorted(sortByAscString), + }, + ...activitiesFilters, + }, + orderBy: activitiesOrderByVariables, + }; + + const existingActivities = readFindManyActivitiesQueryInCache({ + queryVariables: currentFindManyActivitiesQueryVariables, + }); + + const nextActivityIds = [ + ...existingActivityIdsFromTargets, + activityToInject.id, + ]; + + const nextFindManyActivitiesQueryVariables = { + filter: { + id: { + in: nextActivityIds.toSorted(sortByAscString), + }, + ...activitiesFilters, + }, + orderBy: activitiesOrderByVariables, + }; + + const newActivities = [...existingActivities]; + + if (!injectOnlyInIdFilter) { + const newActivity = { + ...activityToInject, + __typename: 'Activity', + }; + + newActivities.unshift(newActivity); + } + + overwriteFindManyActivitiesInCache({ + objectRecordsToOverwrite: newActivities, + queryVariables: nextFindManyActivitiesQueryVariables, + }); + } else { + const currentFindManyActivitiesQueryVariables = { + filter: { + ...activitiesFilters, + }, + orderBy: activitiesOrderByVariables, + }; + + const existingActivities = readFindManyActivitiesQueryInCache({ + queryVariables: currentFindManyActivitiesQueryVariables, + }); + + const nextFindManyActivitiesQueryVariables = { + filter: { + ...activitiesFilters, + }, + orderBy: activitiesOrderByVariables, + }; + + const newActivities = [...existingActivities]; + + if (!injectOnlyInIdFilter) { + const newActivity = { + ...activityToInject, + __typename: 'Activity', + }; + + newActivities.unshift(newActivity); + } + + overwriteFindManyActivitiesInCache({ + objectRecordsToOverwrite: newActivities, + queryVariables: nextFindManyActivitiesQueryVariables, + }); + } + }; + + return { + injectActivitiesQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQuery.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQuery.ts deleted file mode 100644 index 7522844bb..000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivitiesQuery.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; - -import { Activity } from '@/activities/types/Activity'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; -import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { OrderByField } from '@/object-metadata/types/OrderByField'; -import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; -import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; -import { sortByAscString } from '~/utils/array/sortByAscString'; - -// TODO: create a generic hook from this -export const useInjectIntoActivitiesQuery = () => { - const { objectMetadataItem: objectMetadataItemActivity } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.Activity, - }); - - const { - upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const { objectMetadataItem: objectMetadataItemActivityTarget } = - useObjectMetadataItemOnly({ - objectNameSingular: CoreObjectNameSingular.ActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const { - readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, - } = useReadFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivity, - }); - - const { - upsertFindManyRecordsQueryInCache: - overwriteFindManyActivityTargetsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - - const injectActivitiesQueries = ({ - activityToInject, - activityTargetsToInject, - targetableObjects, - activitiesFilters, - activitiesOrderByVariables, - }: { - activityToInject: Activity; - activityTargetsToInject: ActivityTarget[]; - targetableObjects: ActivityTargetableObject[]; - activitiesFilters: ObjectRecordQueryFilter; - activitiesOrderByVariables: OrderByField; - }) => { - const newActivity = { - ...activityToInject, - __typename: 'Activity', - }; - - const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ - targetableObjects, - }); - - const findManyActivitiyTargetsQueryVariables = { - filter: findManyActivitiyTargetsQueryFilter, - }; - - const existingActivityTargets = readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivitiyTargetsQueryVariables, - }); - - const newActivityTargets = [ - ...existingActivityTargets, - ...activityTargetsToInject, - ]; - - const existingActivityIds = existingActivityTargets - ?.map((activityTarget) => activityTarget.activityId) - .filter(isNonEmptyString); - - const currentFindManyActivitiesQueryVariables = { - filter: { - id: { - in: existingActivityIds.toSorted(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - const existingActivities = readFindManyActivitiesQueryInCache({ - queryVariables: currentFindManyActivitiesQueryVariables, - }); - - const nextActivityIds = [...existingActivityIds, newActivity.id]; - - const nextFindManyActivitiesQueryVariables = { - filter: { - id: { - in: nextActivityIds.toSorted(sortByAscString), - }, - ...activitiesFilters, - }, - orderBy: activitiesOrderByVariables, - }; - - overwriteFindManyActivityTargetsQueryInCache({ - objectRecordsToOverwrite: newActivityTargets, - queryVariables: findManyActivitiyTargetsQueryVariables, - }); - - const newActivities = [newActivity, ...existingActivities]; - - overwriteFindManyActivitiesInCache({ - objectRecordsToOverwrite: newActivities, - queryVariables: nextFindManyActivitiesQueryVariables, - }); - }; - - return { - injectActivitiesQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts new file mode 100644 index 000000000..1b70bc053 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useInjectIntoActivityTargetsQueries.ts @@ -0,0 +1,81 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; + +// TODO: create a generic hook from this +export const useInjectIntoActivityTargetsQueries = () => { + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const { + upsertFindManyRecordsQueryInCache: + overwriteFindManyActivityTargetsQueryInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const injectActivityTargetsQueries = ({ + activityTargetsToInject, + targetableObjects, + }: { + activityTargetsToInject: ActivityTarget[]; + targetableObjects: ActivityTargetableObject[]; + }) => { + const hasActivityTargets = isNonEmptyArray(targetableObjects); + + if (!hasActivityTargets) { + return; + } + + const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ + targetableObjects, + }); + + const findManyActivitiyTargetsQueryVariables = { + filter: findManyActivitiyTargetsQueryFilter, + }; + + const existingActivityTargetsWithMaybeDuplicates = + readFindManyActivityTargetsQueryInCache({ + queryVariables: findManyActivitiyTargetsQueryVariables, + }); + + const existingActivityTargetsWithoutDuplicates: ObjectRecord[] = + existingActivityTargetsWithMaybeDuplicates.filter( + (existingActivityTarget) => + !activityTargetsToInject.some( + (activityTargetToInject) => + activityTargetToInject.id === existingActivityTarget.id, + ), + ); + + const newActivityTargets = [ + ...existingActivityTargetsWithoutDuplicates, + ...activityTargetsToInject, + ]; + + overwriteFindManyActivityTargetsQueryInCache({ + objectRecordsToOverwrite: newActivityTargets, + queryVariables: findManyActivitiyTargetsQueryVariables, + }); + }; + + return { + injectActivityTargetsQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts index 13c765656..766d50feb 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivitiesQueries.ts @@ -1,6 +1,5 @@ import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; @@ -9,8 +8,10 @@ import { OrderByField } from '@/object-metadata/types/OrderByField'; import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; import { sortByAscString } from '~/utils/array/sortByAscString'; +// TODO: improve, no bug if query to inject doesn't exist export const useRemoveFromActivitiesQueries = () => { const { objectMetadataItem: objectMetadataItemActivity } = useObjectMetadataItemOnly({ @@ -40,22 +41,13 @@ export const useRemoveFromActivitiesQueries = () => { objectMetadataItem: objectMetadataItemActivity, }); - const { - upsertFindManyRecordsQueryInCache: - overwriteFindManyActivityTargetsQueryInCache, - } = useUpsertFindManyRecordsQueryInCache({ - objectMetadataItem: objectMetadataItemActivityTarget, - }); - const removeFromActivitiesQueries = ({ activityIdToRemove, - activityTargetsToRemove, targetableObjects, activitiesFilters, activitiesOrderByVariables, }: { activityIdToRemove: string; - activityTargetsToRemove: ActivityTarget[]; targetableObjects: ActivityTargetableObject[]; activitiesFilters?: ObjectRecordQueryFilter; activitiesOrderByVariables?: OrderByField; @@ -64,28 +56,15 @@ export const useRemoveFromActivitiesQueries = () => { targetableObjects, }); + const findManyActivityTargetsQueryVariables = { + filter: findManyActivitiyTargetsQueryFilter, + } as ObjectRecordQueryVariables; + const existingActivityTargetsForTargetableObject = readFindManyActivityTargetsQueryInCache({ - queryVariables: findManyActivitiyTargetsQueryFilter, + queryVariables: findManyActivityTargetsQueryVariables, }); - const newActivityTargetsForTargetableObject = isNonEmptyArray( - activityTargetsToRemove, - ) - ? existingActivityTargetsForTargetableObject.filter( - (existingActivityTarget) => - activityTargetsToRemove.some( - (activityTargetToRemove) => - activityTargetToRemove.id !== existingActivityTarget.id, - ), - ) - : existingActivityTargetsForTargetableObject; - - overwriteFindManyActivityTargetsQueryInCache({ - objectRecordsToOverwrite: newActivityTargetsForTargetableObject, - queryVariables: findManyActivitiyTargetsQueryFilter, - }); - const existingActivityIds = existingActivityTargetsForTargetableObject ?.map((activityTarget) => activityTarget.activityId) .filter(isNonEmptyString); @@ -104,6 +83,10 @@ export const useRemoveFromActivitiesQueries = () => { queryVariables: currentFindManyActivitiesQueryVariables, }); + if (!isNonEmptyArray(existingActivities)) { + return; + } + const activityIdsAfterRemoval = existingActivityIds.filter( (existingActivityId) => existingActivityId !== activityIdToRemove, ); diff --git a/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts new file mode 100644 index 000000000..dd490f685 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/hooks/useRemoveFromActivityTargetsQueries.ts @@ -0,0 +1,72 @@ +import { isNonEmptyArray } from '@sniptt/guards'; + +import { ActivityTarget } from '@/activities/types/ActivityTarget'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter'; +import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const useRemoveFromActivityTargetsQueries = () => { + const { objectMetadataItem: objectMetadataItemActivityTarget } = + useObjectMetadataItemOnly({ + objectNameSingular: CoreObjectNameSingular.ActivityTarget, + }); + + const { + readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, + } = useReadFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const { + upsertFindManyRecordsQueryInCache: + overwriteFindManyActivityTargetsQueryInCache, + } = useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItemActivityTarget, + }); + + const removeFromActivityTargetsQueries = ({ + activityTargetsToRemove, + targetableObjects, + }: { + activityTargetsToRemove: ActivityTarget[]; + targetableObjects: ActivityTargetableObject[]; + }) => { + const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({ + targetableObjects, + }); + + const findManyActivityTargetsQueryVariables = { + filter: findManyActivitiyTargetsQueryFilter, + } as ObjectRecordQueryVariables; + + const existingActivityTargetsForTargetableObject = + readFindManyActivityTargetsQueryInCache({ + queryVariables: findManyActivityTargetsQueryVariables, + }); + + const newActivityTargetsForTargetableObject = isNonEmptyArray( + activityTargetsToRemove, + ) + ? existingActivityTargetsForTargetableObject.filter( + (existingActivityTarget) => + activityTargetsToRemove.some( + (activityTargetToRemove) => + activityTargetToRemove.id !== existingActivityTarget.id, + ), + ) + : existingActivityTargetsForTargetableObject; + + overwriteFindManyActivityTargetsQueryInCache({ + objectRecordsToOverwrite: newActivityTargetsForTargetableObject, + queryVariables: findManyActivityTargetsQueryVariables, + }); + }; + + return { + removeFromActivityTargetsQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts index 1156ff001..379013bd6 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -1,16 +1,22 @@ -import { useApolloClient } from '@apollo/client'; +import { useLocation } from 'react-router-dom'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; +import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; +import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries'; +import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; +import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; +import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries'; -import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; import { Activity } from '@/activities/types/Activity'; -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { isDefined } from '~/utils/isDefined'; // TODO: create a generic way to have records only in cache for create mode and delete them afterwards ? export const useUpsertActivity = () => { @@ -30,16 +36,35 @@ export const useUpsertActivity = () => { const setActivityInDrawer = useSetRecoilState(activityInDrawerState); - const timelineTargetableObject = useRecoilValue( - timelineTargetableObjectState, + const objectShowPageTargetableObject = useRecoilValue( + objectShowPageTargetableObjectState, ); + const { injectActivitiesQueries } = useInjectIntoActivitiesQueries(); + const { injectActivityTargetsQueries } = + useInjectIntoActivityTargetsQueries(); + + const { pathname } = useLocation(); + + const weAreOnObjectShowPage = pathname.startsWith('/object'); + const weAreOnTaskPage = pathname.startsWith('/tasks'); + const { injectIntoTimelineActivitiesQueries } = useInjectIntoTimelineActivitiesQueries(); const { makeActivityWithConnection } = useActivityConnectionUtils(); - const apolloClient = useApolloClient(); + const currentCompletedTaskQueryVariables = useRecoilValue( + currentCompletedTaskQueryVariablesState, + ); + + const currentIncompleteTaskQueryVariables = useRecoilValue( + currentIncompleteTaskQueryVariablesState, + ); + + const currentNotesQueryVariables = useRecoilValue( + currentNotesQueryVariablesState, + ); const upsertActivity = async ({ activity, @@ -59,21 +84,91 @@ export const useUpsertActivity = () => { const { activityWithConnection } = makeActivityWithConnection(activityToCreate); + if (weAreOnTaskPage) { + if (isDefined(activityWithConnection.completedAt)) { + injectActivitiesQueries({ + activitiesFilters: currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + activityTargetsToInject: activityToCreate.activityTargets, + activityToInject: activityWithConnection, + targetableObjects: [], + }); + } else { + injectActivitiesQueries({ + activitiesFilters: currentIncompleteTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy, + activityTargetsToInject: activityToCreate.activityTargets, + activityToInject: activityWithConnection, + targetableObjects: [], + }); + } + + injectActivityTargetsQueries({ + activityTargetsToInject: activityToCreate.activityTargets, + targetableObjects: [], + }); + } + // Call optimistic effects - if (timelineTargetableObject) { + if (weAreOnObjectShowPage && objectShowPageTargetableObject) { injectIntoTimelineActivitiesQueries({ - timelineTargetableObject: timelineTargetableObject, + timelineTargetableObject: objectShowPageTargetableObject, activityToInject: activityWithConnection, activityTargetsToInject: activityToCreate.activityTargets, }); + + const injectOnlyInIdFilterForTaskQueries = + activityWithConnection.type !== 'Task'; + + const injectOnlyInIdFilterForNotesQueries = + activityWithConnection.type !== 'Note'; + + if (isDefined(currentCompletedTaskQueryVariables)) { + injectActivitiesQueries({ + activitiesFilters: currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + activityTargetsToInject: activityToCreate.activityTargets, + activityToInject: activityWithConnection, + targetableObjects: [objectShowPageTargetableObject], + injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries, + }); + } + + if (isDefined(currentIncompleteTaskQueryVariables)) { + injectActivitiesQueries({ + activitiesFilters: + currentIncompleteTaskQueryVariables?.filter ?? {}, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy ?? {}, + activityTargetsToInject: activityToCreate.activityTargets, + activityToInject: activityWithConnection, + targetableObjects: [objectShowPageTargetableObject], + injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries, + }); + } + + if (isDefined(currentNotesQueryVariables)) { + injectActivitiesQueries({ + activitiesFilters: currentNotesQueryVariables?.filter, + activitiesOrderByVariables: currentNotesQueryVariables?.orderBy, + activityTargetsToInject: activityToCreate.activityTargets, + activityToInject: activityWithConnection, + targetableObjects: [objectShowPageTargetableObject], + injectOnlyInIdFilter: injectOnlyInIdFilterForNotesQueries, + }); + } + + injectActivityTargetsQueries({ + activityTargetsToInject: activityToCreate.activityTargets, + targetableObjects: [objectShowPageTargetableObject], + }); } await createActivityInDB(activityToCreate); - await apolloClient.refetchQueries({ - include: ['FindManyActivities', 'FindManyActivityTargets'], - }); - setActivityInDrawer(activityToCreate); setIsActivityInCreateMode(false); diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts index 4a5c9b6d7..65b1eae2f 100644 --- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts +++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts @@ -1,18 +1,47 @@ +import { useEffect, useMemo } from 'react'; +import { useRecoilState } from 'recoil'; + import { useActivities } from '@/activities/hooks/useActivities'; +import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; import { Note } from '@/activities/types/Note'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity'; export const useNotes = (targetableObject: ActivityTargetableObject) => { + const notesQueryVariables = useMemo( + () => + ({ + filter: { + type: { eq: 'Note' }, + }, + orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + }) as ObjectRecordQueryVariables, + [], + ); + const { activities, initialized, loading } = useActivities({ - activitiesFilters: { - type: { eq: 'Note' }, - }, - activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + activitiesFilters: notesQueryVariables.filter ?? {}, + activitiesOrderByVariables: notesQueryVariables.orderBy ?? {}, targetableObjects: [targetableObject], }); + const [currentNotesQueryVariables, setCurrentNotesQueryVariables] = + useRecoilState(currentNotesQueryVariablesState); + + // TODO: fix useEffect, remove with better pattern + useEffect(() => { + if (!isDeeplyEqual(notesQueryVariables, currentNotesQueryVariables)) { + setCurrentNotesQueryVariables(notesQueryVariables); + } + }, [ + notesQueryVariables, + currentNotesQueryVariables, + setCurrentNotesQueryVariables, + ]); + return { notes: activities as Note[], initialized, diff --git a/packages/twenty-front/src/modules/activities/notes/states/currentNotesQueryVariablesState.ts b/packages/twenty-front/src/modules/activities/notes/states/currentNotesQueryVariablesState.ts new file mode 100644 index 000000000..9697d0951 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/notes/states/currentNotesQueryVariablesState.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; + +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const currentNotesQueryVariablesState = + atom({ + default: null, + key: 'currentNotesQueryVariablesState', + }); diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx index 2dc31905d..386458955 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx @@ -1,17 +1,23 @@ +import { useLocation } from 'react-router-dom'; import styled from '@emotion/styled'; import { isNonEmptyArray } from '@sniptt/guards'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; +import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries'; +import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries'; +import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; import { activityInDrawerState } from '@/activities/states/activityInDrawerState'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; -import { useRemoveFromTimelineActivitiesQueries } from '@/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries'; -import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState'; +import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; +import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; @@ -37,13 +43,11 @@ export const ActivityActionBar = () => { const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({ objectNameSingular: CoreObjectNameSingular.Activity, - refetchFindManyQuery: true, }); const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords( { objectNameSingular: CoreObjectNameSingular.ActivityTarget, - refetchFindManyQuery: true, }, ); @@ -56,15 +60,34 @@ export const ActivityActionBar = () => { const [isUpsertingActivityInDB] = useRecoilState( isUpsertingActivityInDBState, ); - const timelineTargetableObject = useRecoilValue( - timelineTargetableObjectState, + const objectShowPageTargetableObject = useRecoilValue( + objectShowPageTargetableObjectState, ); const openCreateActivity = useOpenCreateActivityDrawer(); - const { removeFromTimelineActivitiesQueries } = - useRemoveFromTimelineActivitiesQueries(); + const currentCompletedTaskQueryVariables = useRecoilValue( + currentCompletedTaskQueryVariablesState, + ); + + const currentIncompleteTaskQueryVariables = useRecoilValue( + currentIncompleteTaskQueryVariablesState, + ); + + const currentNotesQueryVariables = useRecoilValue( + currentNotesQueryVariablesState, + ); + + const { pathname } = useLocation(); + const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries(); + const { removeFromActivityTargetsQueries } = + useRemoveFromActivityTargetsQueries(); + + const weAreOnObjectShowPage = pathname.startsWith('/object'); + const weAreOnTaskPage = pathname.startsWith('/tasks'); + + const deleteActivity = async () => { + setIsRightDrawerOpen(false); - const deleteActivity = () => { if (viewableActivityId) { if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) { deleteActivityFromCache(temporaryActivityForEditor); @@ -74,22 +97,77 @@ export const ActivityActionBar = () => { const activityTargetIdsToDelete = activityInDrawer?.activityTargets.map(mapToRecordId) ?? []; - if (isDefined(timelineTargetableObject)) { - removeFromTimelineActivitiesQueries({ - activityTargetsToRemove: activityInDrawer?.activityTargets ?? [], + if (weAreOnTaskPage) { + removeFromActivitiesQueries({ activityIdToRemove: viewableActivityId, + targetableObjects: [], + activitiesFilters: currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + }); + + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [], + activitiesFilters: currentIncompleteTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy, + }); + } else if ( + weAreOnObjectShowPage && + isDefined(objectShowPageTargetableObject) + ) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: {}, + activitiesOrderByVariables: + FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + }); + + if (isDefined(currentCompletedTaskQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: currentCompletedTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentCompletedTaskQueryVariables?.orderBy, + }); + } + + if (isDefined(currentIncompleteTaskQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: currentIncompleteTaskQueryVariables?.filter, + activitiesOrderByVariables: + currentIncompleteTaskQueryVariables?.orderBy, + }); + } + + if (isDefined(currentNotesQueryVariables)) { + removeFromActivitiesQueries({ + activityIdToRemove: viewableActivityId, + targetableObjects: [objectShowPageTargetableObject], + activitiesFilters: currentNotesQueryVariables?.filter, + activitiesOrderByVariables: currentNotesQueryVariables?.orderBy, + }); + } + + removeFromActivityTargetsQueries({ + activityTargetsToRemove: activityInDrawer?.activityTargets ?? [], + targetableObjects: [objectShowPageTargetableObject], }); } if (isNonEmptyArray(activityTargetIdsToDelete)) { - deleteManyActivityTargets(activityTargetIdsToDelete); + await deleteManyActivityTargets(activityTargetIdsToDelete); } - deleteOneActivity?.(viewableActivityId); + + await deleteOneActivity?.(viewableActivityId); } } } - - setIsRightDrawerOpen(false); }; const record = useRecoilValue( @@ -98,7 +176,7 @@ export const ActivityActionBar = () => { const addActivity = () => { setIsRightDrawerOpen(false); - if (record && timelineTargetableObject) { + if (record && objectShowPageTargetableObject) { openCreateActivity({ type: record.type, customAssignee: record.assignee, diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx index e7afecc95..fa7616383 100644 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/__stories__/RightDrawerActivityTopBar.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; +import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { RightDrawerActivityTopBar } from '../RightDrawerActivityTopBar'; @@ -14,7 +14,7 @@ const meta: Meta = { ), - ComponentDecorator, + ComponentWithRouterDecorator, ], parameters: { msw: graphqlMocks, diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts index 8c23e7106..33f1d4c1d 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts @@ -1,12 +1,18 @@ +import { useEffect, useMemo } from 'react'; import { isNonEmptyArray } from '@sniptt/guards'; import { DateTime } from 'luxon'; +import { useRecoilState } from 'recoil'; import { useActivities } from '@/activities/hooks/useActivities'; +import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState'; +import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState'; import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; import { parseDate } from '~/utils/date-utils'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type UseTasksProps = { filterDropdownId?: string; @@ -21,27 +27,94 @@ export const useTasks = ({ filterDropdownId, }); - const assigneeIdFilter = selectedFilter - ? { - assigneeId: { - in: JSON.parse(selectedFilter.value), - }, - } - : undefined; + const assigneeIdFilter = useMemo( + () => + selectedFilter + ? { + assigneeId: { + in: JSON.parse(selectedFilter.value), + }, + } + : undefined, + [selectedFilter], + ); const skipActivityTargets = !isNonEmptyArray(targetableObjects); + const completedQueryVariables = useMemo( + () => + ({ + filter: { + completedAt: { is: 'NOT_NULL' }, + type: { eq: 'Task' }, + ...assigneeIdFilter, + }, + orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + }) as ObjectRecordQueryVariables, + [assigneeIdFilter], + ); + + const incompleteQueryVariables = useMemo( + () => + ({ + filter: { + completedAt: { is: 'NULL' }, + type: { eq: 'Task' }, + ...assigneeIdFilter, + }, + orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + }) as ObjectRecordQueryVariables, + [assigneeIdFilter], + ); + + const [ + currentCompletedTaskQueryVariables, + setCurrentCompletedTaskQueryVariables, + ] = useRecoilState(currentCompletedTaskQueryVariablesState); + + const [ + currentIncompleteTaskQueryVariables, + setCurrentIncompleteTaskQueryVariables, + ] = useRecoilState(currentIncompleteTaskQueryVariablesState); + + // TODO: fix useEffect, remove with better pattern + useEffect(() => { + if ( + !isDeeplyEqual( + completedQueryVariables, + currentCompletedTaskQueryVariables, + ) + ) { + setCurrentCompletedTaskQueryVariables(completedQueryVariables); + } + }, [ + completedQueryVariables, + currentCompletedTaskQueryVariables, + setCurrentCompletedTaskQueryVariables, + ]); + + useEffect(() => { + if ( + !isDeeplyEqual( + incompleteQueryVariables, + currentIncompleteTaskQueryVariables, + ) + ) { + setCurrentIncompleteTaskQueryVariables(incompleteQueryVariables); + } + }, [ + incompleteQueryVariables, + currentIncompleteTaskQueryVariables, + setCurrentIncompleteTaskQueryVariables, + ]); + const { activities: completeTasksData, initialized: initializedCompleteTasks, } = useActivities({ targetableObjects, - activitiesFilters: { - completedAt: { is: 'NOT_NULL' }, - type: { eq: 'Task' }, - ...assigneeIdFilter, - }, - activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + activitiesFilters: completedQueryVariables.filter ?? {}, + activitiesOrderByVariables: completedQueryVariables.orderBy ?? {}, skipActivityTargets, }); @@ -50,12 +123,8 @@ export const useTasks = ({ initialized: initializedIncompleteTasks, } = useActivities({ targetableObjects, - activitiesFilters: { - completedAt: { is: 'NULL' }, - type: { eq: 'Task' }, - ...assigneeIdFilter, - }, - activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, + activitiesFilters: incompleteQueryVariables.filter ?? {}, + activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? {}, skipActivityTargets, }); diff --git a/packages/twenty-front/src/modules/activities/tasks/states/currentCompletedTaskQueryVariablesState.ts b/packages/twenty-front/src/modules/activities/tasks/states/currentCompletedTaskQueryVariablesState.ts new file mode 100644 index 000000000..d408d89e3 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/tasks/states/currentCompletedTaskQueryVariablesState.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; + +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const currentCompletedTaskQueryVariablesState = + atom({ + default: null, + key: 'currentCompletedTaskQueryVariablesState', + }); diff --git a/packages/twenty-front/src/modules/activities/tasks/states/currentIncompleteTaskQueryVariablesState.ts b/packages/twenty-front/src/modules/activities/tasks/states/currentIncompleteTaskQueryVariablesState.ts new file mode 100644 index 000000000..9a18ac27a --- /dev/null +++ b/packages/twenty-front/src/modules/activities/tasks/states/currentIncompleteTaskQueryVariablesState.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; + +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export const currentIncompleteTaskQueryVariablesState = + atom({ + default: null, + key: 'currentIncompleteTaskQueryVariablesState', + }); diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx index e9c6c27cb..ffb01a993 100644 --- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx +++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx @@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil'; import { useActivities } from '@/activities/hooks/useActivities'; import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup'; import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; -import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { @@ -44,7 +44,7 @@ export const Timeline = ({ }); const setTimelineTargetableObject = useSetRecoilState( - timelineTargetableObjectState, + objectShowPageTargetableObjectState, ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts index ee637aac1..19539090e 100644 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries.ts @@ -1,10 +1,10 @@ -import { useInjectIntoActivitiesQuery } from '@/activities/hooks/useInjectIntoActivitiesQuery'; +import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries'; import { Activity } from '@/activities/types/Activity'; import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; export const useInjectIntoTimelineActivitiesQueries = () => { - const { injectActivitiesQueries } = useInjectIntoActivitiesQuery(); + const { injectActivitiesQueries } = useInjectIntoActivitiesQueries(); const injectIntoTimelineActivitiesQueries = ({ activityToInject, diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts deleted file mode 100644 index f5572f0f8..000000000 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY'; -import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState'; -import { ActivityTarget } from '@/activities/types/ActivityTarget'; - -export const useRemoveFromTimelineActivitiesQueries = () => { - const timelineTargetableObject = useRecoilValue( - timelineTargetableObjectState, - ); - - // const { objectMetadataItem: objectMetadataItemActivity } = - // useObjectMetadataItemOnly({ - // objectNameSingular: CoreObjectNameSingular.Activity, - // }); - - // const { - // upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache, - // } = useUpsertFindManyRecordsQueryInCache({ - // objectMetadataItem: objectMetadataItemActivity, - // }); - - // const { objectMetadataItem: objectMetadataItemActivityTarget } = - // useObjectMetadataItemOnly({ - // objectNameSingular: CoreObjectNameSingular.ActivityTarget, - // }); - - // const { - // readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache, - // } = useReadFindManyRecordsQueryInCache({ - // objectMetadataItem: objectMetadataItemActivityTarget, - // }); - - // const { - // readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache, - // } = useReadFindManyRecordsQueryInCache({ - // objectMetadataItem: objectMetadataItemActivity, - // }); - - // const { - // upsertFindManyRecordsQueryInCache: - // overwriteFindManyActivityTargetsQueryInCache, - // } = useUpsertFindManyRecordsQueryInCache({ - // objectMetadataItem: objectMetadataItemActivityTarget, - // }); - - const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries(); - - const removeFromTimelineActivitiesQueries = ({ - activityIdToRemove, - activityTargetsToRemove, - }: { - activityIdToRemove: string; - activityTargetsToRemove: ActivityTarget[]; - }) => { - if (!timelineTargetableObject) { - throw new Error('Timeline targetable object is not defined'); - } - - removeFromActivitiesQueries({ - activityIdToRemove, - activityTargetsToRemove, - targetableObjects: [timelineTargetableObject], - activitiesFilters: {}, - activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY, - }); - - // const targetObjectFieldName = getActivityTargetObjectFieldIdName({ - // nameSingular: timelineTargetableObject.targetObjectNameSingular, - // }); - - // const activitiyTargetsForTargetableObjectQueryVariables = { - // filter: { - // [targetObjectFieldName]: { - // eq: timelineTargetableObject.id, - // }, - // }, - // }; - - // const existingActivityTargetsForTargetableObject = - // readFindManyActivityTargetsQueryInCache({ - // queryVariables: activitiyTargetsForTargetableObjectQueryVariables, - // }); - - // const newActivityTargetsForTargetableObject = isNonEmptyArray( - // activityTargetsToRemove, - // ) - // ? existingActivityTargetsForTargetableObject.filter( - // (existingActivityTarget) => - // activityTargetsToRemove.some( - // (activityTargetToRemove) => - // activityTargetToRemove.id !== existingActivityTarget.id, - // ), - // ) - // : existingActivityTargetsForTargetableObject; - - // overwriteFindManyActivityTargetsQueryInCache({ - // objectRecordsToOverwrite: newActivityTargetsForTargetableObject, - // queryVariables: activitiyTargetsForTargetableObjectQueryVariables, - // }); - - // const existingActivityIds = existingActivityTargetsForTargetableObject - // ?.map((activityTarget) => activityTarget.activityId) - // .filter(isNonEmptyString); - - // const timelineActivitiesQueryVariablesBeforeDrawerMount = - // makeTimelineActivitiesQueryVariables({ - // activityIds: existingActivityIds, - // }); - - // const existingActivities = readFindManyActivitiesQueryInCache({ - // queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount, - // }); - - // const activityIdsAfterRemoval = existingActivityIds.filter( - // (existingActivityId) => existingActivityId !== activityIdToRemove, - // ); - - // const timelineActivitiesQueryVariablesAfterRemoval = - // makeTimelineActivitiesQueryVariables({ - // activityIds: activityIdsAfterRemoval, - // }); - - // const newActivities = existingActivities - // .filter((existingActivity) => existingActivity.id !== activityIdToRemove) - // .toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsFirst')); - - // overwriteFindManyActivitiesInCache({ - // objectRecordsToOverwrite: newActivities, - // queryVariables: timelineActivitiesQueryVariablesAfterRemoval, - // }); - }; - - return { - removeFromTimelineActivitiesQueries, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts index 348f693e0..a6b5a4cbb 100644 --- a/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline/hooks/useTimelineActivities.ts @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useRecoilState } from 'recoil'; +import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils'; import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; -import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState'; import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; import { Activity } from '@/activities/types/Activity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -22,15 +22,15 @@ export const useTimelineActivities = ({ }) => { const { makeActivityWithoutConnection } = useActivityConnectionUtils(); - const [, setTimelineTargetableObject] = useRecoilState( - timelineTargetableObjectState, + const [, setObjectShowPageTargetableObject] = useRecoilState( + objectShowPageTargetableObjectState, ); useEffect(() => { if (isDefined(targetableObject)) { - setTimelineTargetableObject(targetableObject); + setObjectShowPageTargetableObject(targetableObject); } - }, [targetableObject, setTimelineTargetableObject]); + }, [targetableObject, setObjectShowPageTargetableObject]); const { activityTargets, diff --git a/packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts b/packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectState.ts similarity index 65% rename from packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts rename to packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectState.ts index ad328f150..730f5fdf2 100644 --- a/packages/twenty-front/src/modules/activities/timeline/states/timelineTargetableObjectState.ts +++ b/packages/twenty-front/src/modules/activities/timeline/states/objectShowPageTargetableObjectState.ts @@ -2,8 +2,8 @@ import { atom } from 'recoil'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -export const timelineTargetableObjectState = +export const objectShowPageTargetableObjectState = atom({ - key: 'timelineTargetableObjectState', + key: 'objectShowPageTargetableObjectState', default: null, }); diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts index 96c5fa0cc..fa544b664 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect.ts @@ -117,7 +117,10 @@ export const triggerUpdateRecordOptimisticEffect = ({ const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy); - if (rootQueryNextEdgesShouldBeSorted) { + if ( + rootQueryNextEdgesShouldBeSorted && + Object.getOwnPropertyNames(rootQueryOrderBy).length > 0 + ) { rootQueryNextEdges = sortCachedObjectEdges({ edges: rootQueryNextEdges, orderBy: rootQueryOrderBy,