From 692005cdb04ff1cdf5b30c634c824cd6a6251a8c Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Wed, 4 Dec 2024 18:16:30 +0100 Subject: [PATCH 1/7] Refetch queries after C UD operations --- .../hooks/useCreateManyRecords.ts | 6 +++++ .../object-record/hooks/useCreateOneRecord.ts | 6 +++++ .../hooks/useDeleteManyRecords.ts | 7 ++++- .../object-record/hooks/useDeleteOneRecord.ts | 7 +++++ .../hooks/useDestroyManyRecords.ts | 6 +++++ .../hooks/useRefetchAggregateQueries.ts | 27 +++++++++++++++++++ .../object-record/hooks/useUpdateOneRecord.ts | 10 ++++++- ...mnHeaderAggregateDropdownFieldsContent.tsx | 2 +- .../utils/generateAggregateQuery.ts | 3 ++- .../utils/getAggregateQueryName.ts | 10 +++++++ 10 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index c236baede835..d2cb0c69ec82 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -11,6 +11,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getCreateManyRecordsMutationResponseField } from '@/object-record/utils/getCreateManyRecordsMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; @@ -51,6 +52,10 @@ export const useCreateManyRecords = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const createManyRecords = async ( recordsToCreate: Partial[], upsert?: boolean, @@ -141,6 +146,7 @@ export const useCreateManyRecords = < throw error; }); + await refetchAggregateQueries(); return createdObjects.data?.[mutationResponseField] ?? []; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index 73c9cd9897d6..3660dbd7e147 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -12,6 +12,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getCreateOneRecordMutationResponseField } from '@/object-record/utils/getCreateOneRecordMutationResponseField'; import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; @@ -55,6 +56,10 @@ export const useCreateOneRecord = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const createOneRecord = async (input: Partial) => { setLoading(true); @@ -131,6 +136,7 @@ export const useCreateOneRecord = < throw error; }); + await refetchAggregateQueries(); return createdObject.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 192b642bf79d..404320943eff 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -9,6 +9,7 @@ import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNo import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { useRecoilValue } from 'recoil'; @@ -51,6 +52,10 @@ export const useDeleteManyRecords = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDeleteManyRecordsMutationResponseField( objectMetadataItem.namePlural, ); @@ -194,7 +199,7 @@ export const useDeleteManyRecords = ({ await sleep(options.delayInMsBetweenRequests); } } - + await refetchAggregateQueries(); return deletedRecords; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 969b830114ce..604b68c62399 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -8,6 +8,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 { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { capitalize } from '~/utils/string/capitalize'; @@ -35,6 +36,10 @@ export const useDeleteOneRecord = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDeleteOneRecordMutationResponseField(objectNameSingular); @@ -126,6 +131,7 @@ export const useDeleteOneRecord = ({ throw error; }); + await refetchAggregateQueries(); return deletedRecord.data?.[mutationResponseField] ?? null; }, [ @@ -135,6 +141,7 @@ export const useDeleteOneRecord = ({ mutationResponseField, objectMetadataItem, objectMetadataItems, + refetchAggregateQueries, ], ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 78895084487e..64fad663356b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -8,6 +8,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordsMutation'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; @@ -48,6 +49,10 @@ export const useDestroyManyRecords = ({ const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const mutationResponseField = getDestroyManyRecordsMutationResponseField( objectMetadataItem.namePlural, ); @@ -127,6 +132,7 @@ export const useDestroyManyRecords = ({ } } + await refetchAggregateQueries(); return destroyedRecords; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts new file mode 100644 index 000000000000..d8b4bde485f8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts @@ -0,0 +1,27 @@ +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useApolloClient } from '@apollo/client'; + +export const useRefetchAggregateQueries = ({ + objectMetadataNamePlural, +}: { + objectMetadataNamePlural: string; +}) => { + const apolloClient = useApolloClient(); + const isAggregateQueryEnabled = useIsFeatureEnabled( + 'IS_AGGREGATE_QUERY_ENABLED', + ); + const refetchAggregateQueries = async () => { + const queryName = getAggregateQueryName(objectMetadataNamePlural); + + if (isAggregateQueryEnabled) { + await apolloClient.refetchQueries({ + include: [queryName], + }); + } + }; + + return { + refetchAggregateQueries, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 8f77eaaee238..ef09ba7fc6d2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -7,6 +7,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 { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField'; @@ -45,6 +46,10 @@ export const useUpdateOneRecord = < const { objectMetadataItems } = useObjectMetadataItems(); + const { refetchAggregateQueries } = useRefetchAggregateQueries({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const updateOneRecord = async ({ idToUpdate, updateOneRecordInput, @@ -116,7 +121,7 @@ export const useUpdateOneRecord = < idToUpdate, input: sanitizedInput, }, - update: (cache, { data }) => { + update: async (cache, { data }) => { const record = data?.[mutationResponseField]; if (!record || !cachedRecord) return; @@ -128,6 +133,8 @@ export const useUpdateOneRecord = < updatedRecord: record, objectMetadataItems, }); + + await refetchAggregateQueries(); }, }) .catch((error: Error) => { @@ -152,6 +159,7 @@ export const useUpdateOneRecord = < throw error; }); + await refetchAggregateQueries(); return updatedRecord?.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx index ea519eb02d41..bd9ef0015c38 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx @@ -41,7 +41,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { if (!fieldMetadata) return null; return ( - + { diff --git a/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts index 476d6fc3786d..d6feb05e5475 100644 --- a/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts +++ b/packages/twenty-front/src/modules/object-record/utils/generateAggregateQuery.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; import { capitalize } from '~/utils/string/capitalize'; export const generateAggregateQuery = ({ @@ -17,7 +18,7 @@ export const generateAggregateQuery = ({ .join('\n '); return gql` - query AggregateMany${capitalize(objectMetadataItem.namePlural)}($filter: ${capitalize( + query ${getAggregateQueryName(objectMetadataItem.namePlural)}($filter: ${capitalize( objectMetadataItem.nameSingular, )}FilterInput) { ${objectMetadataItem.namePlural}(filter: $filter) { diff --git a/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts new file mode 100644 index 000000000000..b24889a395c8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts @@ -0,0 +1,10 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getAggregateQueryName = ( + objectMetadataNamePlural: string, +): string => { + if (!objectMetadataNamePlural) { + throw new Error('objectMetadataNamePlural is required'); + } + return `AggregateMany${capitalize(objectMetadataNamePlural)}`; +}; From 0340a91f8261f619294c4f684c7c35f8c3e4e4c3 Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Wed, 4 Dec 2024 18:25:42 +0100 Subject: [PATCH 2/7] display "-" if no value --- .../RecordBoardColumnHeaderAggregateDropdown.tsx | 2 +- .../RecordBoardColumnHeaderAggregateDropdownButton.tsx | 7 +++++-- .../hooks/useAggregateManyRecordsForRecordBoardColumn.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx index bb1f48a8aa24..ce8911d97dc2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx @@ -11,7 +11,7 @@ import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; type RecordBoardColumnHeaderAggregateDropdownProps = { - aggregateValue: string | number; + aggregateValue?: string | number; aggregateLabel?: string; objectMetadataItem: ObjectMetadataItem; dropdownId: string; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx index d9e52f729129..dd12c11f72fb 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx @@ -13,13 +13,16 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({ tooltip, }: { dropdownId: string; - value: string | number; + value?: string | number; tooltip?: string; }) => { return (
- + { ); return { - aggregateValue: value ?? recordCount, + aggregateValue: isAggregateQueryEnabled ? value : recordCount, aggregateLabel: isDefined(value) ? label : undefined, }; }; From 13061b0c817fec2d05237c04bd934a7d010309dc Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Thu, 5 Dec 2024 11:18:13 +0100 Subject: [PATCH 3/7] Replace -AggregateMany with -Aggregate (remove Many suffix) --- ...AggregateManyRecords.ts => useAggregateRecords.ts} | 11 ++++++----- ...anyRecordsQuery.ts => useAggregateRecordsQuery.ts} | 2 +- .../components/RecordBoardColumnHeader.tsx | 4 ++-- ...oardColumnHeaderAggregateDropdownFieldsContent.tsx | 1 - ....ts => useAggregateRecordsForRecordBoardColumn.ts} | 6 +++--- .../utils/computeAggregateValueAndLabel.ts | 4 ++-- .../utils/__tests__/generateAggregateQuery.test.ts | 4 ++-- .../object-record/utils/getAggregateQueryName.ts | 5 +++-- 8 files changed, 19 insertions(+), 18 deletions(-) rename packages/twenty-front/src/modules/object-record/hooks/{useAggregateManyRecords.ts => useAggregateRecords.ts} (85%) rename packages/twenty-front/src/modules/object-record/hooks/{useAggregateManyRecordsQuery.ts => useAggregateRecordsQuery.ts} (97%) rename packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/{useAggregateManyRecordsForRecordBoardColumn.ts => useAggregateRecordsForRecordBoardColumn.ts} (94%) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts similarity index 85% rename from packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts rename to packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts index 8fc9e313fbb2..8415e6d16a1e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts @@ -4,18 +4,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; -import { useAggregateManyRecordsQuery } from '@/object-record/hooks/useAggregateManyRecordsQuery'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import isEmpty from 'lodash.isempty'; import { isDefined } from 'twenty-ui'; -export type AggregateManyRecordsData = { +export type AggregateRecordsData = { [fieldName: string]: { [operation in AGGREGATE_OPERATIONS]?: string | number | undefined; }; }; -export const useAggregateManyRecords = ({ +export const useAggregateRecords = ({ objectNameSingular, filter, recordGqlFieldsAggregate, @@ -30,9 +30,10 @@ export const useAggregateManyRecords = ({ objectNameSingular, }); - const { aggregateQuery, gqlFieldToFieldMap } = useAggregateManyRecordsQuery({ + const { aggregateQuery, gqlFieldToFieldMap } = useAggregateRecordsQuery({ objectNameSingular, recordGqlFieldsAggregate, + filter, }); const { data, loading, error } = useQuery( @@ -45,7 +46,7 @@ export const useAggregateManyRecords = ({ }, ); - const formattedData: AggregateManyRecordsData = {}; + const formattedData: AggregateRecordsData = {}; if (!isEmpty(data)) { Object.entries(data?.[objectMetadataItem.namePlural] ?? {})?.forEach( diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts rename to packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts index ae47c346a5f5..e9002e826b25 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts @@ -14,7 +14,7 @@ export type GqlFieldToFieldMap = { ]; }; -export const useAggregateManyRecordsQuery = ({ +export const useAggregateRecordsQuery = ({ objectNameSingular, recordGqlFieldsAggregate = {}, }: { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index f07b3e4cd3ca..6935b6b35cd9 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -6,7 +6,7 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { useAggregateManyRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn'; +import { useAggregateRecordsForRecordBoardColumn } from '@/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn'; import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; @@ -96,7 +96,7 @@ export const RecordBoardColumnHeader = () => { }; const { aggregateValue, aggregateLabel } = - useAggregateManyRecordsForRecordBoardColumn(); + useAggregateRecordsForRecordBoardColumn(); const { handleNewButtonClick } = useColumnNewCardActions( columnDefinition.id ?? '', diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx index bd9ef0015c38..1e7c66bf81bc 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx @@ -43,7 +43,6 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { return ( { updateViewAggregate({ kanbanAggregateOperationFieldMetadataId: fieldId, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts similarity index 94% rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts rename to packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index 85d7fd82f804..b16db61e0ced 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateManyRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -1,4 +1,4 @@ -import { useAggregateManyRecords } from '@/object-record/hooks/useAggregateManyRecords'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { buildRecordGqlFieldsAggregate } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate'; @@ -13,7 +13,7 @@ import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; -export const useAggregateManyRecordsForRecordBoardColumn = () => { +export const useAggregateRecordsForRecordBoardColumn = () => { const isAggregateQueryEnabled = useIsFeatureEnabled( 'IS_AGGREGATE_QUERY_ENABLED', ); @@ -67,7 +67,7 @@ export const useAggregateManyRecordsForRecordBoardColumn = () => { : { eq: columnDefinition.value }, }; - const { data } = useAggregateManyRecords({ + const { data } = useAggregateRecords({ objectNameSingular: objectMetadataItem.nameSingular, recordGqlFieldsAggregate, filter, diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts index 9140d840581b..52ef2b86d4cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts @@ -1,5 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { AggregateManyRecordsData } from '@/object-record/hooks/useAggregateManyRecords'; +import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; @@ -8,7 +8,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const computeAggregateValueAndLabel = ( - data: AggregateManyRecordsData, + data: AggregateRecordsData, objectMetadataItem: ObjectMetadataItem, recordIndexKanbanAggregateOperation: KanbanAggregateOperation, kanbanFieldName: string, diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts index a1f6c7aaea87..4ce449957125 100644 --- a/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/generateAggregateQuery.test.ts @@ -35,7 +35,7 @@ describe('generateAggregateQuery', () => { const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim(); expect(normalizedQuery).toBe( - 'query AggregateManyCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { id name createdAt } }', + 'query AggregateCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { id name createdAt } }', ); }); @@ -69,7 +69,7 @@ describe('generateAggregateQuery', () => { const normalizedQuery = result.loc?.source.body.replace(/\s+/g, ' ').trim(); expect(normalizedQuery).toBe( - 'query AggregateManyPeople($filter: PersonFilterInput) { people(filter: $filter) { id } }', + 'query AggregatePeople($filter: PersonFilterInput) { people(filter: $filter) { id } }', ); }); }); diff --git a/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts index b24889a395c8..e8b48509d9f6 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getAggregateQueryName.ts @@ -1,10 +1,11 @@ +import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; export const getAggregateQueryName = ( objectMetadataNamePlural: string, ): string => { - if (!objectMetadataNamePlural) { + if (!isDefined(objectMetadataNamePlural)) { throw new Error('objectMetadataNamePlural is required'); } - return `AggregateMany${capitalize(objectMetadataNamePlural)}`; + return `Aggregate${capitalize(objectMetadataNamePlural)}`; }; From b775f1b9a56d5520baa1f57ea054989c66fe76f5 Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Thu, 5 Dec 2024 11:18:27 +0100 Subject: [PATCH 4/7] minor improvements --- .../modules/object-record/hooks/useRefetchAggregateQueries.ts | 4 ++-- .../src/modules/object-record/hooks/useUpdateOneRecord.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts index d8b4bde485f8..dee30f513f8b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRefetchAggregateQueries.ts @@ -12,9 +12,9 @@ export const useRefetchAggregateQueries = ({ 'IS_AGGREGATE_QUERY_ENABLED', ); const refetchAggregateQueries = async () => { - const queryName = getAggregateQueryName(objectMetadataNamePlural); - if (isAggregateQueryEnabled) { + const queryName = getAggregateQueryName(objectMetadataNamePlural); + await apolloClient.refetchQueries({ include: [queryName], }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index ef09ba7fc6d2..4127bab0b87c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -121,7 +121,7 @@ export const useUpdateOneRecord = < idToUpdate, input: sanitizedInput, }, - update: async (cache, { data }) => { + update: (cache, { data }) => { const record = data?.[mutationResponseField]; if (!record || !cachedRecord) return; @@ -133,8 +133,6 @@ export const useUpdateOneRecord = < updatedRecord: record, objectMetadataItems, }); - - await refetchAggregateQueries(); }, }) .catch((error: Error) => { From 2ce546668cb92f35a1c9e5544dad3555d9e41df2 Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Thu, 5 Dec 2024 11:18:37 +0100 Subject: [PATCH 5/7] add tests --- .../__tests__/useCreateManyRecords.test.tsx | 11 +++ .../__tests__/useCreateOneRecord.test.tsx | 11 +++ .../__tests__/useDeleteManyRecords.test.tsx | 11 +++ .../__tests__/useDeleteOneRecord.test.tsx | 11 +++ .../useRefetchAggregateQueries.test.tsx | 77 +++++++++++++++++++ .../__tests__/useUpdateOneRecord.test.tsx | 12 +++ .../__tests__/getAggregateQueryName.test.ts | 23 ++++++ 7 files changed, 156 insertions(+) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx index 761680c43081..82ff949fef9e 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx @@ -9,12 +9,19 @@ import { variables, } from '@/object-record/hooks/__mocks__/useCreateManyRecords'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('uuid', () => ({ v4: jest.fn(), })); +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + mocked(v4) .mockReturnValueOnce(variables.data[0].id) .mockReturnValueOnce(variables.data[1].id); @@ -40,6 +47,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ }); describe('useCreateManyRecords', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => @@ -57,5 +67,6 @@ describe('useCreateManyRecords', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx index db26b860517a..0caea681d9b5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx @@ -6,6 +6,7 @@ import { responseData, } from '@/object-record/hooks/__mocks__/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; @@ -15,6 +16,12 @@ jest.mock('uuid', () => ({ v4: jest.fn(() => personId), })); +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const mocks = [ { request: { @@ -34,6 +41,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ }); describe('useCreateOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => @@ -52,5 +62,6 @@ describe('useCreateOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx index 89d5d120592e..2f6f68b1542c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx @@ -6,6 +6,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { act } from 'react'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; @@ -28,11 +29,20 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); describe('useDeleteManyRecords', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useDeleteManyRecords({ objectNameSingular: 'person' }), @@ -48,5 +58,6 @@ describe('useDeleteManyRecords', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx index 0c347d309520..1a5b65bc99ff 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx @@ -7,6 +7,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; @@ -25,11 +26,20 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); describe('useDeleteOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useDeleteOneRecord({ objectNameSingular: 'person' }), @@ -45,5 +55,6 @@ describe('useDeleteOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx new file mode 100644 index 000000000000..78d56ced24cf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRefetchAggregateQueries.test.tsx @@ -0,0 +1,77 @@ +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useApolloClient } from '@apollo/client'; +import { renderHook } from '@testing-library/react'; + +jest.mock('@apollo/client', () => ({ + useApolloClient: jest.fn(), +})); + +jest.mock('@/workspace/hooks/useIsFeatureEnabled', () => ({ + useIsFeatureEnabled: jest.fn(), +})); + +describe('useRefetchAggregateQueries', () => { + const mockRefetchQueries = jest.fn(); + const mockApolloClient = { + refetchQueries: mockRefetchQueries, + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useApolloClient as jest.Mock).mockReturnValue(mockApolloClient); + }); + + it('should refetch queries when feature flag is enabled', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(true); + const objectMetadataNamePlural = 'opportunities'; + const expectedQueryName = getAggregateQueryName(objectMetadataNamePlural); + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + await result.current.refetchAggregateQueries(); + + // Assert + expect(mockRefetchQueries).toHaveBeenCalledTimes(1); + expect(mockRefetchQueries).toHaveBeenCalledWith({ + include: [expectedQueryName], + }); + }); + + it('should not refetch queries when feature flag is disabled', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(false); + const objectMetadataNamePlural = 'opportunities'; + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + await result.current.refetchAggregateQueries(); + + // Assert + expect(mockRefetchQueries).not.toHaveBeenCalled(); + }); + + it('should handle errors during refetch', async () => { + // Arrange + (useIsFeatureEnabled as jest.Mock).mockReturnValue(true); + const error = new Error('Refetch failed'); + mockRefetchQueries.mockRejectedValue(error); + const objectMetadataNamePlural = 'opportunities'; + + // Act + const { result } = renderHook(() => + useRefetchAggregateQueries({ objectMetadataNamePlural }), + ); + + // Assert + await expect(result.current.refetchAggregateQueries()).rejects.toThrow( + 'Refetch failed', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx index d32ef37508b1..557892a1f38a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx @@ -5,7 +5,9 @@ import { responseData, variables, } from '@/object-record/hooks/__mocks__/useUpdateOneRecord'; +import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { expect } from '@storybook/test'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; @@ -35,6 +37,12 @@ const mocks = [ }, ]; +jest.mock('@/object-record/hooks/useRefetchAggregateQueries'); +const mockRefetchAggregateQueries = jest.fn(); +(useRefetchAggregateQueries as jest.Mock).mockReturnValue({ + refetchAggregateQueries: mockRefetchAggregateQueries, +}); + const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks, }); @@ -42,6 +50,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ const idToUpdate = '36abbb63-34ed-4a16-89f5-f549ac55d0f9'; describe('useUpdateOneRecord', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); it('works as expected', async () => { const { result } = renderHook( () => useUpdateOneRecord({ objectNameSingular: 'person' }), @@ -61,5 +72,6 @@ describe('useUpdateOneRecord', () => { }); expect(mocks[0].result).toHaveBeenCalled(); + expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts new file mode 100644 index 000000000000..b6ca9e632e37 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/getAggregateQueryName.test.ts @@ -0,0 +1,23 @@ +import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName'; + +describe('getAggregateQueryName', () => { + it('should return the correct aggregate query name for a valid plural name', () => { + expect(getAggregateQueryName('opportunities')).toBe( + 'AggregateOpportunities', + ); + expect(getAggregateQueryName('companies')).toBe('AggregateCompanies'); + expect(getAggregateQueryName('people')).toBe('AggregatePeople'); + }); + + it('should throw an error when input is undefined', () => { + expect(() => getAggregateQueryName(undefined as any)).toThrow( + 'objectMetadataNamePlural is required', + ); + }); + + it('should throw an error when input is null', () => { + expect(() => getAggregateQueryName(null as any)).toThrow( + 'objectMetadataNamePlural is required', + ); + }); +}); From 86dec8b083c28e3ea0794acb59527fb0c294f8d6 Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Thu, 5 Dec 2024 14:41:13 +0100 Subject: [PATCH 6/7] some fixes + tests --- .../graphql/types/RecordGqlFieldsAggregate.ts | 2 +- .../hooks/__mocks__/useAggregateRecords.ts | 19 +++ .../__tests__/useAggregateRecords.test.tsx | 129 ++++++++++++++++ .../useAggregateRecordsQuery.test.tsx | 144 ++++++++++++++++++ .../hooks/useAggregateRecords.ts | 1 - .../hooks/useAggregateRecordsQuery.ts | 34 ++--- ...mnHeaderAggregateDropdownFieldsContent.tsx | 23 +-- ...lumnHeaderAggregateDropdownMenuContent.tsx | 27 ++-- .../utils/buildRecordGqlFieldsAggregate.ts | 10 +- 9 files changed, 337 insertions(+), 52 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts index 7217a34dccc3..990a73181d4d 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts @@ -1,3 +1,3 @@ import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; -export type RecordGqlFieldsAggregate = Record; +export type RecordGqlFieldsAggregate = Record; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts new file mode 100644 index 000000000000..b590efc57ab5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; + +export const AGGREGATE_QUERY = gql` + query AggregateOpportunities($filter: OpportunityFilterInput) { + opportunities(filter: $filter) { + totalCount + sumAmount + avgAmount + } + } +`; + +export const mockResponse = { + opportunities: { + totalCount: 42, + sumAmount: 1000000, + avgAmount: 23800 + } +}; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx new file mode 100644 index 000000000000..5eab4810f555 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx @@ -0,0 +1,129 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { + AGGREGATE_QUERY, + mockResponse, +} from '@/object-record/hooks/__mocks__/useAggregateRecords'; +import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { useQuery } from '@apollo/client'; +import { renderHook } from '@testing-library/react'; + +// Mocks +jest.mock('@apollo/client'); +jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); +jest.mock('@/object-record/hooks/useAggregateRecordsQuery'); + +const mockObjectMetadataItem = { + nameSingular: 'opportunity', + namePlural: 'opportunities', +}; + +const mockGqlFieldToFieldMap = { + sumAmount: ['amount', AGGREGATE_OPERATIONS.sum], + avgAmount: ['amount', AGGREGATE_OPERATIONS.avg], + totalCount: ['name', AGGREGATE_OPERATIONS.count], +}; + +describe('useAggregateRecords', () => { + beforeEach(() => { + (useObjectMetadataItem as jest.Mock).mockReturnValue({ + objectMetadataItem: mockObjectMetadataItem, + }); + + (useAggregateRecordsQuery as jest.Mock).mockReturnValue({ + aggregateQuery: AGGREGATE_QUERY, + gqlFieldToFieldMap: mockGqlFieldToFieldMap, + }); + + (useQuery as jest.Mock).mockReturnValue({ + data: mockResponse, + loading: false, + error: undefined, + }); + }); + + it('should format data correctly', () => { + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum, AGGREGATE_OPERATIONS.avg], + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.data).toEqual({ + amount: { + [AGGREGATE_OPERATIONS.sum]: 1000000, + [AGGREGATE_OPERATIONS.avg]: 23800, + }, + name: { + [AGGREGATE_OPERATIONS.count]: 42, + }, + }); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBeUndefined(); + }); + + it('should handle loading state', () => { + (useQuery as jest.Mock).mockReturnValue({ + data: undefined, + loading: true, + error: undefined, + }); + + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.data).toEqual({}); + expect(result.current.loading).toBe(true); + }); + + it('should handle error state', () => { + const mockError = new Error('Query failed'); + (useQuery as jest.Mock).mockReturnValue({ + data: undefined, + loading: false, + error: mockError, + }); + + const { result } = renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.data).toEqual({}); + expect(result.current.error).toBe(mockError); + }); + + it('should skip query when specified', () => { + renderHook(() => + useAggregateRecords({ + objectNameSingular: 'opportunity', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + skip: true, + }), + ); + + expect(useQuery).toHaveBeenCalledWith( + AGGREGATE_QUERY, + expect.objectContaining({ + skip: true, + }), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx new file mode 100644 index 000000000000..6157455bf3cf --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx @@ -0,0 +1,144 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery'; +import { renderHook } from '@testing-library/react'; +import { FieldMetadataType } from '~/generated/graphql'; + +jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); +jest.mock('@/object-record/utils/generateAggregateQuery'); + +const mockObjectMetadataItem: ObjectMetadataItem = { + nameSingular: 'company', + namePlural: 'companies', + id: 'test-id', + labelSingular: 'Company', + labelPlural: 'Companies', + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + fields: [ + { + id: 'field-1', + name: 'amount', + label: 'Amount', + type: FieldMetadataType.Number, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, + { + id: 'field-2', + name: 'name', + label: 'Name', + type: FieldMetadataType.Text, + isCustom: false, + isActive: true, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } as FieldMetadataItem, + ], + indexMetadatas: [], + isLabelSyncedWithName: true, + isRemote: false, + isSystem: false, +}; + +describe('useAggregateRecordsQuery', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useObjectMetadataItem as jest.Mock).mockReturnValue({ + objectMetadataItem: mockObjectMetadataItem, + }); + + (generateAggregateQuery as jest.Mock).mockReturnValue({ + loc: { + source: { + body: 'query AggregateCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { totalCount } }', + }, + }, + }); + }); + + it('should handle simple count operation', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toEqual({ + totalCount: ['name', 'COUNT'], + }); + expect(generateAggregateQuery).toHaveBeenCalledWith({ + objectMetadataItem: mockObjectMetadataItem, + recordGqlFields: { + totalCount: true, + }, + }); + }); + + it('should handle field aggregation', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toEqual({ + sumAmount: ['amount', 'SUM'], + }); + expect(generateAggregateQuery).toHaveBeenCalledWith( + expect.objectContaining({ + recordGqlFields: expect.objectContaining({ + sumAmount: true, + }), + }), + ); + }); + + it('should throw error for invalid aggregation operation', () => { + expect(() => + renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + name: [AGGREGATE_OPERATIONS.sum], + }, + }), + ), + ).toThrow(); + }); + + it('should handle multiple aggregations', () => { + const { result } = renderHook(() => + useAggregateRecordsQuery({ + objectNameSingular: 'company', + recordGqlFieldsAggregate: { + amount: [AGGREGATE_OPERATIONS.sum], + name: [AGGREGATE_OPERATIONS.count], + }, + }), + ); + + expect(result.current.gqlFieldToFieldMap).toHaveProperty('sumAmount'); + expect(generateAggregateQuery).toHaveBeenCalledWith( + expect.objectContaining({ + recordGqlFields: expect.objectContaining({ + totalCount: true, + sumAmount: true, + }), + }), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts index 8415e6d16a1e..2a3b6357897a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts @@ -33,7 +33,6 @@ export const useAggregateRecords = ({ const { aggregateQuery, gqlFieldToFieldMap } = useAggregateRecordsQuery({ objectNameSingular, recordGqlFieldsAggregate, - filter, }); const { data, loading, error } = useQuery( diff --git a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts index e9002e826b25..ea87546fe88b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useAggregateRecordsQuery.ts @@ -34,26 +34,20 @@ export const useAggregateRecordsQuery = ({ const gqlFieldToFieldMap: GqlFieldToFieldMap = {}; Object.entries(recordGqlFieldsAggregate).forEach( - ([fieldName, aggregateOperation]) => { - if ( - !isDefined(fieldName) && - aggregateOperation === AGGREGATE_OPERATIONS.count - ) { - recordGqlFields.totalCount = true; - return; - } - - const fieldToQuery = - availableAggregations[fieldName]?.[aggregateOperation]; - - if (!isDefined(fieldToQuery)) { - throw new Error( - `Cannot query operation ${aggregateOperation} on field ${fieldName}`, - ); - } - gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; - - recordGqlFields[fieldToQuery] = true; + ([fieldName, aggregateOperations]) => { + aggregateOperations.forEach((aggregateOperation) => { + const fieldToQuery = + availableAggregations[fieldName]?.[aggregateOperation]; + + if (!isDefined(fieldToQuery)) { + throw new Error( + `Cannot query operation ${aggregateOperation} on field ${fieldName}`, + ); + } + gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation]; + + recordGqlFields[fieldToQuery] = true; + }); }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx index 1e7c66bf81bc..995b48eb2c1e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx @@ -34,15 +34,16 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { {getAggregateOperationLabel(aggregateOperation)} - {availableFieldsIdsForAggregateOperation.map((fieldId) => { - const fieldMetadata = objectMetadataItem.fields.find( - (field) => field.id === fieldId, - ); - - if (!fieldMetadata) return null; - return ( - + + {availableFieldsIdsForAggregateOperation.map((fieldId) => { + const fieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === fieldId, + ); + + if (!fieldMetadata) return null; + return ( { updateViewAggregate({ kanbanAggregateOperationFieldMetadataId: fieldId, @@ -53,9 +54,9 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => { LeftIcon={getIcon(fieldMetadata.icon) ?? Icon123} text={fieldMetadata.label} /> - - ); - })} + ); + })} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx index 1d36b8aeb337..eb1ab6dcbe54 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx @@ -66,19 +66,16 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { }} text={getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)} /> - - {Object.entries(availableAggregations).map( - ([ - availableAggregationOperation, - availableAggregationFieldsIdsForOperation, - ]) => - isEmpty(availableAggregationFieldsIdsForOperation) ? ( - <> - ) : ( - + {Object.entries(availableAggregations).map( + ([ + availableAggregationOperation, + availableAggregationFieldsIdsForOperation, + ]) => + isEmpty(availableAggregationFieldsIdsForOperation) ? ( + <> + ) : ( { setAggregateOperation( availableAggregationOperation as AGGREGATE_OPERATIONS, @@ -93,9 +90,9 @@ export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => { )} hasSubMenu /> - - ), - )} + ), + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts index f7bca49306e9..d759868f94b4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregate.ts @@ -1,4 +1,5 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlFieldsAggregate } from '@/object-record/graphql/types/RecordGqlFieldsAggregate'; import { KanbanAggregateOperation } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { isDefined } from '~/utils/isDefined'; @@ -11,7 +12,7 @@ export const buildRecordGqlFieldsAggregate = ({ objectMetadataItem: ObjectMetadataItem; recordIndexKanbanAggregateOperation: KanbanAggregateOperation; kanbanFieldName: string; -}) => { +}): RecordGqlFieldsAggregate => { let recordGqlFieldsAggregate = {}; const kanbanAggregateOperationFieldName = objectMetadataItem.fields?.find( @@ -30,14 +31,15 @@ export const buildRecordGqlFieldsAggregate = ({ ); } else { recordGqlFieldsAggregate = { - [kanbanFieldName]: AGGREGATE_OPERATIONS.count, + [kanbanFieldName]: [AGGREGATE_OPERATIONS.count], }; } } else { recordGqlFieldsAggregate = { - [kanbanAggregateOperationFieldName]: + [kanbanAggregateOperationFieldName]: [ recordIndexKanbanAggregateOperation?.operation ?? - AGGREGATE_OPERATIONS.count, + AGGREGATE_OPERATIONS.count, + ], }; } From 4bc1d428f985ad1a4f779de3c6c38ba5d5044e50 Mon Sep 17 00:00:00 2001 From: Marie Stoppa Date: Thu, 5 Dec 2024 14:49:14 +0100 Subject: [PATCH 7/7] fix test --- .../utils/__tests__/buildRecordGqlFieldsAggregate.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts index 62696fcd6611..85e4c5994e8a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/buildRecordGqlFieldsAggregate.test.ts @@ -57,7 +57,7 @@ describe('buildRecordGqlFieldsAggregate', () => { }); expect(result).toEqual({ - amount: AGGREGATE_OPERATIONS.sum, + amount: [AGGREGATE_OPERATIONS.sum], }); }); @@ -74,7 +74,7 @@ describe('buildRecordGqlFieldsAggregate', () => { }); expect(result).toEqual({ - [MOCK_KANBAN_FIELD]: AGGREGATE_OPERATIONS.count, + [MOCK_KANBAN_FIELD]: [AGGREGATE_OPERATIONS.count], }); });