Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix favorites #3138

Merged
merged 12 commits into from
Jan 3, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ export const useOptimisticEffect = ({
variables,
});

if (!existingData) {
if (
!existingData &&
(isNonEmptyArray(updatedRecords) ||
charlesBochet marked this conversation as resolved.
Show resolved Hide resolved
isNonEmptyArray(deletedRecordIds))
) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <></>;

Expand Down
289 changes: 117 additions & 172 deletions packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts
Original file line number Diff line number Diff line change
@@ -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';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel we could introduce an enum here for all standard objects

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you already have one: StandardObjectNameSingular

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in another PR

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<Required<Favorite>>) => {
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<Favorite>({
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<string, any>,
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) {
thaisguigon marked this conversation as resolved.
Show resolved Hide resolved
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,
};
};
Original file line number Diff line number Diff line change
@@ -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,
});
};
};
Loading
Loading