diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts index 56991d06943e..15062b335ef8 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts @@ -84,7 +84,11 @@ export const useOptimisticEffect = ({ variables, }); - if (!existingData) { + if ( + !existingData && + (isNonEmptyArray(updatedRecords) || + isNonEmptyArray(deletedRecordIds)) + ) { return; } diff --git a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx index c76f8efb67d7..0feeb57852d1 100644 --- a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx @@ -15,9 +15,7 @@ const StyledContainer = styled(NavigationDrawerSection)` `; export const Favorites = () => { - const { favorites, handleReorderFavorite } = useFavorites({ - objectNamePlural: 'companies', - }); + const { favorites, handleReorderFavorite } = useFavorites(); if (!favorites || favorites.length === 0) return <>; diff --git a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts index 06f2ef70827f..cc98a87bc006 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts @@ -1,215 +1,160 @@ -import { useApolloClient } from '@apollo/client'; +import { useMemo } from 'react'; import { OnDragEndResponder } from '@hello-pangea/dnd'; -import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; -import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; -import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { Favorite } from '@/favorites/types/Favorite'; -import { mapFavorites } from '@/favorites/utils/mapFavorites'; +import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; -import { favoritesState } from '../states/favoritesState'; - -export const useFavorites = ({ - objectNamePlural, -}: { - objectNamePlural: string; -}) => { +export const useFavorites = () => { const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const [favorites, setFavorites] = useRecoilState(favoritesState); - - const { - updateOneRecordMutation: updateOneFavoriteMutation, - createOneRecordMutation: createOneFavoriteMutation, - deleteOneRecordMutation: deleteOneFavoriteMutation, - } = useObjectMetadataItem({ - objectNameSingular: CoreObjectNameSingular.Favorite, - }); - - const { triggerOptimisticEffects } = useOptimisticEffect({ - objectNameSingular: CoreObjectNameSingular.Favorite, - }); - const { performOptimisticEvict } = useOptimisticEvict(); + const favoriteObjectNameSingular = 'favorite'; - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural, - }); - - const { objectMetadataItem: favoriteTargetObjectMetadataItem } = + const { objectMetadataItem: favoriteObjectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, + objectNameSingular: favoriteObjectNameSingular, }); - const apolloClient = useApolloClient(); - - useFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.Favorite, - onCompleted: useRecoilCallback( - ({ snapshot, set }) => - async (data: PaginatedRecordTypeResults>) => { - const favorites = snapshot.getLoadable(favoritesState).getValue(); + const { deleteOneRecord } = useDeleteOneRecord({ + objectNameSingular: favoriteObjectNameSingular, + }); - const queriedFavorites = mapFavorites( - data.edges.map((edge) => edge.node), - ); + const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({ + objectNameSingular: favoriteObjectNameSingular, + }); - if (!isDeeplyEqual(favorites, queriedFavorites)) { - set(favoritesState, queriedFavorites); - } - }, - [], - ), + const { createOneRecord: createOneFavorite } = useCreateOneRecord({ + objectNameSingular: favoriteObjectNameSingular, }); - const createFavorite = useRecoilCallback( - ({ snapshot, set }) => - async (favoriteTargetObjectId: string, additionalData?: any) => { - const favorites = snapshot.getLoadable(favoritesState).getValue(); + const { records: favorites } = useFindManyRecords({ + objectNameSingular: favoriteObjectNameSingular, + }); - if (!favoriteTargetObjectMetadataItem) { - return; - } - const targetObjectName = favoriteTargetObjectMetadataItem.nameSingular; - - const result = await apolloClient.mutate({ - mutation: createOneFavoriteMutation, - variables: { - input: { - [`${targetObjectName}Id`]: favoriteTargetObjectId, - position: favorites.length + 1, - workspaceMemberId: currentWorkspaceMember?.id, - }, - }, - }); - - triggerOptimisticEffects({ - typename: `FavoriteEdge`, - createdRecords: [result.data[`createFavorite`]], - }); - - const createdFavorite = result?.data?.createFavorite; - - const newFavorite = { - ...additionalData, - ...createdFavorite, - }; - - const newFavoritesMapped = mapFavorites([newFavorite]); - - if (createdFavorite) { - set(favoritesState, [...favorites, ...newFavoritesMapped]); - } - }, - [ - apolloClient, - createOneFavoriteMutation, - currentWorkspaceMember?.id, - favoriteTargetObjectMetadataItem, - triggerOptimisticEffects, - ], + const favoriteRelationFieldMetadataItems = useMemo( + () => + favoriteObjectMetadataItem.fields.filter( + (fieldMetadataItem) => + fieldMetadataItem.type === FieldMetadataType.Relation && + fieldMetadataItem.name !== 'workspaceMember', + ), + [favoriteObjectMetadataItem.fields], ); - const _updateFavoritePosition = useRecoilCallback( - ({ snapshot, set }) => - async (favoriteToUpdate: Favorite) => { - const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState); - const favorites = favoritesStateFromSnapshot.getValue(); - const result = await apolloClient.mutate({ - mutation: updateOneFavoriteMutation, - variables: { - input: { - position: favoriteToUpdate?.position, - }, - idToUpdate: favoriteToUpdate?.id, - }, - }); - - const updatedFavorite = result?.data?.updateFavoriteV2; - if (updatedFavorite) { - set( - favoritesState, - favorites.map((favorite: Favorite) => - favorite.id === updatedFavorite.id ? favoriteToUpdate : favorite, - ), - ); + const getObjectRecordIdentifierByNameSingular = + useGetObjectRecordIdentifierByNameSingular(); + + const favoritesSorted = useMemo(() => { + return favorites + .map((favorite) => { + for (const relationField of favoriteRelationFieldMetadataItems) { + if (isDefined(favorite[relationField.name])) { + const relationObject = favorite[relationField.name]; + + const relationObjectNameSingular = + relationField.toRelationMetadata?.fromObjectMetadata + .nameSingular ?? ''; + + const objectRecordIdentifier = + getObjectRecordIdentifierByNameSingular( + relationObject, + relationObjectNameSingular, + ); + + return { + id: favorite.id, + recordId: objectRecordIdentifier.id, + position: favorite.position, + avatarType: objectRecordIdentifier.avatarType, + avatarUrl: objectRecordIdentifier.avatarUrl, + labelIdentifier: objectRecordIdentifier.name, + link: objectRecordIdentifier.linkToShowPage, + } as Favorite; + } } - }, - [apolloClient, updateOneFavoriteMutation], - ); - - const deleteFavorite = useRecoilCallback( - ({ snapshot, set }) => - async (favoriteIdToDelete: string) => { - const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState); - const favorites = favoritesStateFromSnapshot.getValue(); - const idToDelete = favorites.find( - (favorite: Favorite) => favorite.recordId === favoriteIdToDelete, - )?.id; - - await apolloClient.mutate({ - mutation: deleteOneFavoriteMutation, - variables: { - idToDelete: idToDelete, - }, - }); - - performOptimisticEvict('Favorite', 'id', idToDelete ?? ''); - - set( - favoritesState, - favorites.filter((favorite: Favorite) => favorite.id !== idToDelete), - ); - }, - [apolloClient, deleteOneFavoriteMutation, performOptimisticEvict], - ); - const computeNewPosition = (destIndex: number, sourceIndex: number) => { - if (destIndex === 0) { - return favorites[destIndex].position / 2; - } + return favorite; + }) + .toSorted((a, b) => a.position - b.position); + }, [ + favoriteRelationFieldMetadataItems, + favorites, + getObjectRecordIdentifierByNameSingular, + ]); + + const createFavorite = ( + targetObject: Record, + targetObjectNameSingular: string, + ) => { + createOneFavorite({ + [`${targetObjectNameSingular}Id`]: targetObject.id, + [`${targetObjectNameSingular}`]: targetObject, + position: favorites.length + 1, + workspaceMemberId: currentWorkspaceMember?.id, + }); + }; - if (destIndex === favorites.length - 1) { - return favorites[destIndex - 1].position + 1; - } + const deleteFavorite = (favoriteId: string) => { + deleteOneRecord(favoriteId); + }; - if (sourceIndex < destIndex) { + const computeNewPosition = (destIndex: number, sourceIndex: number) => { + const moveToFirstPosition = destIndex === 0; + const moveToLastPosition = destIndex === favoritesSorted.length - 1; + const moveAfterSource = destIndex > sourceIndex; + + if (moveToFirstPosition) { + return favoritesSorted[0].position / 2; + } else if (moveToLastPosition) { + return favoritesSorted[destIndex - 1].position + 1; + } else if (moveAfterSource) { + return ( + (favoritesSorted[destIndex + 1].position + + favoritesSorted[destIndex].position) / + 2 + ); + } else { return ( - (favorites[destIndex + 1].position + favorites[destIndex].position) / 2 + favoritesSorted[destIndex].position - + (favoritesSorted[destIndex].position - + favoritesSorted[destIndex - 1].position) / + 2 ); } - - return ( - (favorites[destIndex - 1].position + favorites[destIndex].position) / 2 - ); }; const handleReorderFavorite: OnDragEndResponder = (result) => { - if (!result.destination || !favorites) { + if (!result.destination || !favoritesSorted) { return; } + const newPosition = computeNewPosition( result.destination.index, result.source.index, ); - const reorderFavorites = Array.from(favorites); - const [removed] = reorderFavorites.splice(result.source.index, 1); - const removedFav = { ...removed, position: newPosition }; - reorderFavorites.splice(result.destination.index, 0, removedFav); - setFavorites(reorderFavorites); - _updateFavoritePosition(removedFav); + const updatedFavorite = favoritesSorted[result.source.index]; + + updateOneFavorite({ + idToUpdate: updatedFavorite.id, + updateOneRecordInput: { + position: newPosition, + }, + }); }; + return { - favorites, + favorites: favoritesSorted, createFavorite, - deleteFavorite, handleReorderFavorite, + deleteFavorite, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts new file mode 100644 index 000000000000..be32e2162949 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts @@ -0,0 +1,26 @@ +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; +import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; + +export const useGetObjectRecordIdentifierByNameSingular = () => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + return (record: any, objectNameSingular: string): ObjectRecordIdentifier => { + const objectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + if (!objectMetadataItem) { + throw new Error( + `ObjectMetadataItem not found for objectNameSingular: ${objectNameSingular}`, + ); + } + + return getObjectRecordIdentifier({ + objectMetadataItem, + record, + }); + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts index 32c4fa650671..35a1c6d4e89c 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useMapToObjectRecordIdentifier.ts @@ -1,8 +1,6 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { getLogoUrlFromDomainName } from '~/utils'; export const useMapToObjectRecordIdentifier = ({ objectMetadataItem, @@ -10,60 +8,9 @@ export const useMapToObjectRecordIdentifier = ({ objectMetadataItem: ObjectMetadataItem; }) => { return (record: any): ObjectRecordIdentifier => { - switch (objectMetadataItem.nameSingular) { - case CoreObjectNameSingular.Opportunity: - return { - id: record.id, - name: record?.company?.name, - avatarUrl: record.avatarUrl, - avatarType: 'rounded', - }; - } - - const labelIdentifierFieldMetadata = objectMetadataItem.fields.find( - (field) => - field.id === objectMetadataItem.labelIdentifierFieldMetadataId || - field.name === 'name', - ); - - let labelIdentifierFieldValue = ''; - - switch (labelIdentifierFieldMetadata?.type) { - case FieldMetadataType.FullName: { - labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${ - record.name?.lastName ?? '' - }`; - break; - } - default: - labelIdentifierFieldValue = labelIdentifierFieldMetadata - ? record[labelIdentifierFieldMetadata.name] - : ''; - } - - const imageIdentifierFieldMetadata = objectMetadataItem.fields.find( - (field) => field.id === objectMetadataItem.imageIdentifierFieldMetadataId, - ); - - const imageIdentifierFieldValue = imageIdentifierFieldMetadata - ? (record[imageIdentifierFieldMetadata.name] as string) - : null; - - const avatarType = - objectMetadataItem.nameSingular === CoreObjectNameSingular.Company - ? 'squared' - : 'rounded'; - - const avatarUrl = - objectMetadataItem.nameSingular === CoreObjectNameSingular.Company - ? getLogoUrlFromDomainName(record['domainName'] ?? '') - : imageIdentifierFieldValue ?? null; - - return { - id: record.id, - name: labelIdentifierFieldValue, - avatarUrl, - avatarType, - }; + return getObjectRecordIdentifier({ + objectMetadataItem, + record, + }); }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/types/StandardObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/StandardObjectNameSingular.ts new file mode 100644 index 000000000000..3d60d3c76709 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/StandardObjectNameSingular.ts @@ -0,0 +1,5 @@ +export enum StandardObjectNameSingular { + Company = 'company', + Person = 'person', + Opportunity = 'opportunity', +} diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts new file mode 100644 index 000000000000..e42a9de52530 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts @@ -0,0 +1,73 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; +import { FieldMetadataType } from '~/generated/graphql'; +import { getLogoUrlFromDomainName } from '~/utils'; + +export const getObjectRecordIdentifier = ({ + objectMetadataItem, + record, +}: { + objectMetadataItem: ObjectMetadataItem; + record: any; +}): ObjectRecordIdentifier => { + const basePathToShowPage = `/object/${objectMetadataItem.nameSingular}/`; + const linkToShowPage = `${basePathToShowPage}${record.id}`; + + if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity) { + return { + id: record.id, + name: record?.company?.name, + avatarUrl: record.avatarUrl, + avatarType: 'rounded', + linkToShowPage, + }; + } + + const labelIdentifierFieldMetadata = objectMetadataItem.fields.find( + (field) => + field.id === objectMetadataItem.labelIdentifierFieldMetadataId || + field.name === 'name', + ); + + let labelIdentifierFieldValue = ''; + + switch (labelIdentifierFieldMetadata?.type) { + case FieldMetadataType.FullName: { + labelIdentifierFieldValue = `${record.name?.firstName ?? ''} ${ + record.name?.lastName ?? '' + }`; + break; + } + default: + labelIdentifierFieldValue = labelIdentifierFieldMetadata + ? record[labelIdentifierFieldMetadata.name] + : ''; + } + + const imageIdentifierFieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === objectMetadataItem.imageIdentifierFieldMetadataId, + ); + + const imageIdentifierFieldValue = imageIdentifierFieldMetadata + ? (record[imageIdentifierFieldMetadata.name] as string) + : null; + + const avatarType = + objectMetadataItem.nameSingular === CoreObjectNameSingular.Company + ? 'squared' + : 'rounded'; + + const avatarUrl = + objectMetadataItem.nameSingular === CoreObjectNameSingular.Company + ? getLogoUrlFromDomainName(record['domainName'] ?? '') + : imageIdentifierFieldValue ?? null; + + return { + id: record.id, + name: labelIdentifierFieldValue, + avatarUrl, + avatarType, + linkToShowPage, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isStandardObject.ts b/packages/twenty-front/src/modules/object-metadata/utils/isStandardObject.ts new file mode 100644 index 000000000000..d5c1e78a2485 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/isStandardObject.ts @@ -0,0 +1,11 @@ +import { StandardObjectNameSingular } from '@/object-metadata/types/StandardObjectNameSingular'; + +export const isStandardObject = (objectNameSingular: string) => { + const standardObjectNames = [ + StandardObjectNameSingular.Company, + StandardObjectNameSingular.Person, + StandardObjectNameSingular.Opportunity, + ] as string[]; + + return standardObjectNames.includes(objectNameSingular); +}; diff --git a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx index 784adc47fcd4..5452b4e6348f 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordShowPage.tsx @@ -36,7 +36,7 @@ import { FileFolder, useUploadImageMutation, } from '~/generated/graphql'; -import { getLogoUrlFromDomainName } from '~/utils'; +import { isDefined } from '~/utils/isDefined'; import { useFindOneRecord } from '../hooks/useFindOneRecord'; import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord'; @@ -58,9 +58,7 @@ export const RecordShowPage = () => { const { identifiersMapper } = useRelationPicker(); - const { favorites, createFavorite, deleteFavorite } = useFavorites({ - objectNamePlural: objectMetadataItem.namePlural, - }); + const { favorites, createFavorite, deleteFavorite } = useFavorites(); const [, setEntityFields] = useRecoilState( entityFieldsFamilyState(objectRecordId ?? ''), @@ -97,34 +95,19 @@ export const RecordShowPage = () => { return [updateEntity, { loading: false }]; }; - const isFavorite = objectNameSingular - ? favorites.some((favorite) => favorite.recordId === record?.id) - : false; + const correspondingFavorite = favorites.find( + (favorite) => favorite.recordId === objectRecordId, + ); + + const isFavorite = isDefined(correspondingFavorite); const handleFavoriteButtonClick = async () => { if (!objectNameSingular || !record) return; - if (isFavorite) deleteFavorite(record?.id); - else { - const additionalData = - objectNameSingular === 'person' - ? { - labelIdentifier: - record.name.firstName + ' ' + record.name.lastName, - avatarUrl: record.avatarUrl, - avatarType: 'rounded', - link: `/object/personV2/${record.id}`, - recordId: record.id, - } - : objectNameSingular === 'company' - ? { - labelIdentifier: record.name, - avatarUrl: getLogoUrlFromDomainName(record.domainName ?? ''), - avatarType: 'squared', - link: `/object/companyV2/${record.id}`, - recordId: record.id, - } - : {}; - createFavorite(record.id, additionalData); + + if (isFavorite && record) { + deleteFavorite(correspondingFavorite.id); + } else { + createFavorite(record, objectNameSingular); } }; diff --git a/packages/twenty-front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts b/packages/twenty-front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts index 447c8b4faaab..e622e0c1f425 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts @@ -61,7 +61,7 @@ export const getRecordOptimisticEffectDefinition = ({ } } - if (deletedRecordIds) { + if (isNonEmptyArray(deletedRecordIds)) { draft.edges = draft.edges.filter( (edge) => !deletedRecordIds.includes(edge.node.id), ); 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 1a67d1fb017b..4bb189787c32 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -7,7 +7,9 @@ import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMeta import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord'; import { capitalize } from '~/utils/string/capitalize'; -export const useCreateManyRecords = >({ +export const useCreateManyRecords = < + T extends Record & { id: string }, +>({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { const { triggerOptimisticEffects } = useOptimisticEffect({ @@ -62,16 +64,16 @@ export const useCreateManyRecords = >({ } const createdRecords = - (createdObjects.data[ + createdObjects.data[ `create${capitalize(objectMetadataItem.namePlural)}` - ] as T[]) ?? []; + ] ?? []; triggerOptimisticEffects({ typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, createdRecords, }); - return createdRecords; + return createdRecords as T[]; }; return { createManyRecords }; 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 17640c6d9ef8..141c1cff1284 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -4,6 +4,7 @@ import { v4 } from 'uuid'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord'; +import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { capitalize } from '~/utils/string/capitalize'; type useCreateOneRecordProps = { @@ -39,17 +40,20 @@ export const useCreateOneRecord = ({ ...input, }); - if (generatedEmptyRecord) { - triggerOptimisticEffects({ - typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, - createdRecords: [generatedEmptyRecord], - }); - } + const sanitizedUpdateOneRecordInput = sanitizeRecordInput({ + objectMetadataItem, + recordInput: input, + }); + + triggerOptimisticEffects({ + typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, + createdRecords: [generatedEmptyRecord], + }); const createdObject = await apolloClient.mutate({ mutation: createOneRecordMutation, variables: { - input: { id: recordId, ...input }, + input: { id: recordId, ...sanitizedUpdateOneRecordInput }, }, optimisticResponse: { [`create${capitalize(objectMetadataItem.nameSingular)}`]: diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index 18d472aba8d4..b23157f58de0 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -79,9 +79,9 @@ export const useFindManyRecords = < >(findManyRecordsQuery, { skip: skip || !objectMetadataItem || !currentWorkspace, variables: { - filter: filter ?? {}, - limit: limit, - orderBy: orderBy ?? {}, + filter, + limit, + orderBy, }, onCompleted: (data) => { onCompleted?.(data[objectMetadataItem.namePlural]); @@ -116,8 +116,8 @@ export const useFindManyRecords = < try { await fetchMore({ variables: { - filter: filter ?? {}, - orderBy: orderBy ?? {}, + filter, + orderBy, lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined, }, updateQuery: (prev, { fetchMoreResult }) => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts index d7f1b90a7fd8..1c04168ea9ff 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts @@ -18,7 +18,8 @@ export const useGenerateEmptyRecord = ({ validatedInput[fieldMetadataItem.name] ?? generateEmptyFieldValue(fieldMetadataItem); } - return emptyRecord as T; + + return emptyRecord; }; return { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx b/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx index a669b7923a84..748a7871d8a9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx @@ -5,6 +5,7 @@ import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -50,6 +51,8 @@ export const useRecordTableContextMenuEntries = ( objectNamePlural, }); + const { createFavorite, favorites, deleteFavorite } = useFavorites(); + const objectMetadataType = objectNameSingular === 'company' ? 'Company' @@ -57,10 +60,6 @@ export const useRecordTableContextMenuEntries = ( ? 'Person' : 'Custom'; - const { createFavorite, deleteFavorite, favorites } = useFavorites({ - objectNamePlural, - }); - const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => { const selectedRowIds = snapshot .getLoadable(selectedRowIdsSelector) @@ -68,16 +67,22 @@ export const useRecordTableContextMenuEntries = ( const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : ''; - const isFavorite = - !!selectedRowId && - !!favorites?.find((favorite) => favorite.recordId === selectedRowId); + const selectedRecord = snapshot + .getLoadable(entityFieldsFamilyState(selectedRowId)) + .getValue(); + + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === selectedRowId, + ); + + const isFavorite = !!selectedRowId && !!foundFavorite; resetTableRowSelection(); if (isFavorite) { - deleteFavorite(selectedRowId); - } else { - createFavorite(selectedRowId); + deleteFavorite(foundFavorite.id); + } else if (selectedRecord) { + createFavorite(selectedRecord, objectNameSingular); } }); 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 136abacbe6cc..b192ecfd7c5c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput'; import { capitalize } from '~/utils/string/capitalize'; type useUpdateOneRecordProps = { @@ -37,17 +37,10 @@ export const useUpdateOneRecord = ({ ...updateOneRecordInput, }; - const sanitizedUpdateOneRecordInput = Object.fromEntries( - Object.keys(updateOneRecordInput) - .filter((fieldName) => { - const fieldDefinition = objectMetadataItem.fields.find( - (field) => field.name === fieldName, - ); - - return fieldDefinition?.type !== FieldMetadataType.Relation; - }) - .map((fieldName) => [fieldName, updateOneRecordInput[fieldName]]), - ); + const sanitizedUpdateOneRecordInput = sanitizeRecordInput({ + objectMetadataItem, + recordInput: updateOneRecordInput, + }); triggerOptimisticEffects({ typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts index 41dafa7a23e8..50856064c14b 100644 --- a/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordIdentifier.ts @@ -5,4 +5,5 @@ export type ObjectRecordIdentifier = { name: string; avatarUrl?: string | null; avatarType?: AvatarType | null; + linkToShowPage?: string; }; diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts new file mode 100644 index 000000000000..07ea228d2e1a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -0,0 +1,20 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { FieldMetadataType } from '~/generated/graphql'; + +export const sanitizeRecordInput = ({ + objectMetadataItem, + recordInput, +}: { + objectMetadataItem: ObjectMetadataItem; + recordInput: Record; +}) => { + return Object.fromEntries( + Object.entries(recordInput).filter(([fieldName]) => { + const fieldDefinition = objectMetadataItem.fields.find( + (field) => field.name === fieldName, + ); + + return fieldDefinition?.type !== FieldMetadataType.Relation; + }), + ); +};