Fix favorites (#3138)

* WIP

* Finished cleaning favorites create, update, delete on record show page

* Fixed context menu favorite

* Fixed relation field bug

* Fix from review

* Review

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau 2024-01-03 12:30:24 +01:00 committed by GitHub
parent 41f3a74bf4
commit 6797f013c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 317 additions and 299 deletions

View File

@ -84,7 +84,11 @@ export const useOptimisticEffect = ({
variables,
});
if (!existingData) {
if (
!existingData &&
(isNonEmptyArray(updatedRecords) ||
isNonEmptyArray(deletedRecordIds))
) {
return;
}

View File

@ -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 <></>;

View File

@ -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 favoriteObjectNameSingular = 'favorite';
const {
updateOneRecordMutation: updateOneFavoriteMutation,
createOneRecordMutation: createOneFavoriteMutation,
deleteOneRecordMutation: deleteOneFavoriteMutation,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular: CoreObjectNameSingular.Favorite,
});
const { performOptimisticEvict } = useOptimisticEvict();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem: favoriteTargetObjectMetadataItem } =
const { objectMetadataItem: favoriteObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular,
objectNameSingular: favoriteObjectNameSingular,
});
const apolloClient = useApolloClient();
const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular: favoriteObjectNameSingular,
});
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Favorite,
onCompleted: useRecoilCallback(
({ snapshot, set }) =>
async (data: PaginatedRecordTypeResults<Required<Favorite>>) => {
const favorites = snapshot.getLoadable(favoritesState).getValue();
const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({
objectNameSingular: favoriteObjectNameSingular,
});
const queriedFavorites = mapFavorites(
data.edges.map((edge) => edge.node),
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
objectNameSingular: favoriteObjectNameSingular,
});
const { records: favorites } = useFindManyRecords<Favorite>({
objectNameSingular: favoriteObjectNameSingular,
});
const favoriteRelationFieldMetadataItems = useMemo(
() =>
favoriteObjectMetadataItem.fields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.name !== 'workspaceMember',
),
[favoriteObjectMetadataItem.fields],
);
if (!isDeeplyEqual(favorites, queriedFavorites)) {
set(favoritesState, queriedFavorites);
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;
}
},
[],
),
});
const createFavorite = useRecoilCallback(
({ snapshot, set }) =>
async (favoriteTargetObjectId: string, additionalData?: any) => {
const favorites = snapshot.getLoadable(favoritesState).getValue();
if (!favoriteTargetObjectMetadataItem) {
return;
}
const targetObjectName = favoriteTargetObjectMetadataItem.nameSingular;
const result = await apolloClient.mutate({
mutation: createOneFavoriteMutation,
variables: {
input: {
[`${targetObjectName}Id`]: favoriteTargetObjectId,
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,
},
},
});
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 _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,
),
);
}
},
[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 deleteFavorite = (favoriteId: string) => {
deleteOneRecord(favoriteId);
};
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
if (destIndex === 0) {
return favorites[destIndex].position / 2;
}
const moveToFirstPosition = destIndex === 0;
const moveToLastPosition = destIndex === favoritesSorted.length - 1;
const moveAfterSource = destIndex > sourceIndex;
if (destIndex === favorites.length - 1) {
return favorites[destIndex - 1].position + 1;
}
if (sourceIndex < destIndex) {
if (moveToFirstPosition) {
return favoritesSorted[0].position / 2;
} else if (moveToLastPosition) {
return favoritesSorted[destIndex - 1].position + 1;
} else if (moveAfterSource) {
return (
(favorites[destIndex + 1].position + favorites[destIndex].position) / 2
(favoritesSorted[destIndex + 1].position +
favoritesSorted[destIndex].position) /
2
);
} else {
return (
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,
};
};

View File

@ -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,
});
};
};

View File

@ -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,
});
};
};

View File

@ -0,0 +1,5 @@
export enum StandardObjectNameSingular {
Company = 'company',
Person = 'person',
Opportunity = 'opportunity',
}

View File

@ -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,
};
};

View File

@ -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);
};

View File

@ -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);
}
};

View File

@ -61,7 +61,7 @@ export const getRecordOptimisticEffectDefinition = ({
}
}
if (deletedRecordIds) {
if (isNonEmptyArray(deletedRecordIds)) {
draft.edges = draft.edges.filter(
(edge) => !deletedRecordIds.includes(edge.node.id),
);

View File

@ -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 = <T extends Record<string, unknown>>({
export const useCreateManyRecords = <
T extends Record<string, unknown> & { id: string },
>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
@ -62,16 +64,16 @@ export const useCreateManyRecords = <T extends Record<string, unknown>>({
}
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 };

View File

@ -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 = <T>({
...input,
});
if (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)}`]:

View File

@ -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 }) => {

View File

@ -18,7 +18,8 @@ export const useGenerateEmptyRecord = ({
validatedInput[fieldMetadataItem.name] ??
generateEmptyFieldValue(fieldMetadataItem);
}
return emptyRecord as T;
return emptyRecord;
};
return {

View File

@ -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);
}
});

View File

@ -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 = <T>({
...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`,

View File

@ -5,4 +5,5 @@ export type ObjectRecordIdentifier = {
name: string;
avatarUrl?: string | null;
avatarType?: AvatarType | null;
linkToShowPage?: string;
};

View File

@ -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<string, unknown>;
}) => {
return Object.fromEntries(
Object.entries(recordInput).filter(([fieldName]) => {
const fieldDefinition = objectMetadataItem.fields.find(
(field) => field.name === fieldName,
);
return fieldDefinition?.type !== FieldMetadataType.Relation;
}),
);
};