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:
@ -84,7 +84,11 @@ export const useOptimisticEffect = ({
|
|||||||
variables,
|
variables,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingData) {
|
if (
|
||||||
|
!existingData &&
|
||||||
|
(isNonEmptyArray(updatedRecords) ||
|
||||||
|
isNonEmptyArray(deletedRecordIds))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,7 @@ const StyledContainer = styled(NavigationDrawerSection)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Favorites = () => {
|
export const Favorites = () => {
|
||||||
const { favorites, handleReorderFavorite } = useFavorites({
|
const { favorites, handleReorderFavorite } = useFavorites();
|
||||||
objectNamePlural: 'companies',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!favorites || favorites.length === 0) return <></>;
|
if (!favorites || favorites.length === 0) return <></>;
|
||||||
|
|
||||||
|
|||||||
@ -1,215 +1,160 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useMemo } from 'react';
|
||||||
import { OnDragEndResponder } from '@hello-pangea/dnd';
|
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 { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { Favorite } from '@/favorites/types/Favorite';
|
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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { favoritesState } from '../states/favoritesState';
|
export const useFavorites = () => {
|
||||||
|
|
||||||
export const useFavorites = ({
|
|
||||||
objectNamePlural,
|
|
||||||
}: {
|
|
||||||
objectNamePlural: string;
|
|
||||||
}) => {
|
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
const [favorites, setFavorites] = useRecoilState(favoritesState);
|
const favoriteObjectNameSingular = 'favorite';
|
||||||
|
|
||||||
const {
|
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||||
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 } =
|
|
||||||
useObjectMetadataItem({
|
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 queriedFavorites = mapFavorites(
|
|
||||||
data.edges.map((edge) => edge.node),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDeeplyEqual(favorites, queriedFavorites)) {
|
|
||||||
set(favoritesState, queriedFavorites);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createFavorite = useRecoilCallback(
|
const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({
|
||||||
({ snapshot, set }) =>
|
objectNameSingular: favoriteObjectNameSingular,
|
||||||
async (favoriteTargetObjectId: string, additionalData?: any) => {
|
});
|
||||||
const favorites = snapshot.getLoadable(favoritesState).getValue();
|
|
||||||
|
|
||||||
if (!favoriteTargetObjectMetadataItem) {
|
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
|
||||||
return;
|
objectNameSingular: favoriteObjectNameSingular,
|
||||||
}
|
});
|
||||||
const targetObjectName = favoriteTargetObjectMetadataItem.nameSingular;
|
|
||||||
|
|
||||||
const result = await apolloClient.mutate({
|
const { records: favorites } = useFindManyRecords<Favorite>({
|
||||||
mutation: createOneFavoriteMutation,
|
objectNameSingular: favoriteObjectNameSingular,
|
||||||
variables: {
|
});
|
||||||
input: {
|
|
||||||
[`${targetObjectName}Id`]: favoriteTargetObjectId,
|
|
||||||
position: favorites.length + 1,
|
|
||||||
workspaceMemberId: currentWorkspaceMember?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
const favoriteRelationFieldMetadataItems = useMemo(
|
||||||
typename: `FavoriteEdge`,
|
() =>
|
||||||
createdRecords: [result.data[`createFavorite`]],
|
favoriteObjectMetadataItem.fields.filter(
|
||||||
});
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||||
const createdFavorite = result?.data?.createFavorite;
|
fieldMetadataItem.name !== 'workspaceMember',
|
||||||
|
),
|
||||||
const newFavorite = {
|
[favoriteObjectMetadataItem.fields],
|
||||||
...additionalData,
|
|
||||||
...createdFavorite,
|
|
||||||
};
|
|
||||||
|
|
||||||
const newFavoritesMapped = mapFavorites([newFavorite]);
|
|
||||||
|
|
||||||
if (createdFavorite) {
|
|
||||||
set(favoritesState, [...favorites, ...newFavoritesMapped]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
apolloClient,
|
|
||||||
createOneFavoriteMutation,
|
|
||||||
currentWorkspaceMember?.id,
|
|
||||||
favoriteTargetObjectMetadataItem,
|
|
||||||
triggerOptimisticEffects,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const _updateFavoritePosition = useRecoilCallback(
|
const getObjectRecordIdentifierByNameSingular =
|
||||||
({ snapshot, set }) =>
|
useGetObjectRecordIdentifierByNameSingular();
|
||||||
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;
|
const favoritesSorted = useMemo(() => {
|
||||||
if (updatedFavorite) {
|
return favorites
|
||||||
set(
|
.map((favorite) => {
|
||||||
favoritesState,
|
for (const relationField of favoriteRelationFieldMetadataItems) {
|
||||||
favorites.map((favorite: Favorite) =>
|
if (isDefined(favorite[relationField.name])) {
|
||||||
favorite.id === updatedFavorite.id ? favoriteToUpdate : favorite,
|
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(
|
return favorite;
|
||||||
({ snapshot, set }) =>
|
})
|
||||||
async (favoriteIdToDelete: string) => {
|
.toSorted((a, b) => a.position - b.position);
|
||||||
const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState);
|
}, [
|
||||||
const favorites = favoritesStateFromSnapshot.getValue();
|
favoriteRelationFieldMetadataItems,
|
||||||
const idToDelete = favorites.find(
|
favorites,
|
||||||
(favorite: Favorite) => favorite.recordId === favoriteIdToDelete,
|
getObjectRecordIdentifierByNameSingular,
|
||||||
)?.id;
|
]);
|
||||||
|
|
||||||
await apolloClient.mutate({
|
const createFavorite = (
|
||||||
mutation: deleteOneFavoriteMutation,
|
targetObject: Record<string, any>,
|
||||||
variables: {
|
targetObjectNameSingular: string,
|
||||||
idToDelete: idToDelete,
|
) => {
|
||||||
},
|
createOneFavorite({
|
||||||
});
|
[`${targetObjectNameSingular}Id`]: targetObject.id,
|
||||||
|
[`${targetObjectNameSingular}`]: targetObject,
|
||||||
|
position: favorites.length + 1,
|
||||||
|
workspaceMemberId: currentWorkspaceMember?.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
performOptimisticEvict('Favorite', 'id', idToDelete ?? '');
|
const deleteFavorite = (favoriteId: string) => {
|
||||||
|
deleteOneRecord(favoriteId);
|
||||||
set(
|
};
|
||||||
favoritesState,
|
|
||||||
favorites.filter((favorite: Favorite) => favorite.id !== idToDelete),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[apolloClient, deleteOneFavoriteMutation, performOptimisticEvict],
|
|
||||||
);
|
|
||||||
|
|
||||||
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
|
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
|
||||||
if (destIndex === 0) {
|
const moveToFirstPosition = destIndex === 0;
|
||||||
return favorites[destIndex].position / 2;
|
const moveToLastPosition = destIndex === favoritesSorted.length - 1;
|
||||||
}
|
const moveAfterSource = destIndex > sourceIndex;
|
||||||
|
|
||||||
if (destIndex === favorites.length - 1) {
|
if (moveToFirstPosition) {
|
||||||
return favorites[destIndex - 1].position + 1;
|
return favoritesSorted[0].position / 2;
|
||||||
}
|
} else if (moveToLastPosition) {
|
||||||
|
return favoritesSorted[destIndex - 1].position + 1;
|
||||||
if (sourceIndex < destIndex) {
|
} else if (moveAfterSource) {
|
||||||
return (
|
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) => {
|
const handleReorderFavorite: OnDragEndResponder = (result) => {
|
||||||
if (!result.destination || !favorites) {
|
if (!result.destination || !favoritesSorted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPosition = computeNewPosition(
|
const newPosition = computeNewPosition(
|
||||||
result.destination.index,
|
result.destination.index,
|
||||||
result.source.index,
|
result.source.index,
|
||||||
);
|
);
|
||||||
|
|
||||||
const reorderFavorites = Array.from(favorites);
|
const updatedFavorite = favoritesSorted[result.source.index];
|
||||||
const [removed] = reorderFavorites.splice(result.source.index, 1);
|
|
||||||
const removedFav = { ...removed, position: newPosition };
|
updateOneFavorite({
|
||||||
reorderFavorites.splice(result.destination.index, 0, removedFav);
|
idToUpdate: updatedFavorite.id,
|
||||||
setFavorites(reorderFavorites);
|
updateOneRecordInput: {
|
||||||
_updateFavoritePosition(removedFav);
|
position: newPosition,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favorites,
|
favorites: favoritesSorted,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
deleteFavorite,
|
|
||||||
handleReorderFavorite,
|
handleReorderFavorite,
|
||||||
|
deleteFavorite,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||||
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { getLogoUrlFromDomainName } from '~/utils';
|
|
||||||
|
|
||||||
export const useMapToObjectRecordIdentifier = ({
|
export const useMapToObjectRecordIdentifier = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -10,60 +8,9 @@ export const useMapToObjectRecordIdentifier = ({
|
|||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
return (record: any): ObjectRecordIdentifier => {
|
return (record: any): ObjectRecordIdentifier => {
|
||||||
switch (objectMetadataItem.nameSingular) {
|
return getObjectRecordIdentifier({
|
||||||
case CoreObjectNameSingular.Opportunity:
|
objectMetadataItem,
|
||||||
return {
|
record,
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
export enum StandardObjectNameSingular {
|
||||||
|
Company = 'company',
|
||||||
|
Person = 'person',
|
||||||
|
Opportunity = 'opportunity',
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
};
|
||||||
@ -36,7 +36,7 @@ import {
|
|||||||
FileFolder,
|
FileFolder,
|
||||||
useUploadImageMutation,
|
useUploadImageMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { getLogoUrlFromDomainName } from '~/utils';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { useFindOneRecord } from '../hooks/useFindOneRecord';
|
import { useFindOneRecord } from '../hooks/useFindOneRecord';
|
||||||
import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord';
|
||||||
@ -58,9 +58,7 @@ export const RecordShowPage = () => {
|
|||||||
|
|
||||||
const { identifiersMapper } = useRelationPicker();
|
const { identifiersMapper } = useRelationPicker();
|
||||||
|
|
||||||
const { favorites, createFavorite, deleteFavorite } = useFavorites({
|
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
||||||
objectNamePlural: objectMetadataItem.namePlural,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, setEntityFields] = useRecoilState(
|
const [, setEntityFields] = useRecoilState(
|
||||||
entityFieldsFamilyState(objectRecordId ?? ''),
|
entityFieldsFamilyState(objectRecordId ?? ''),
|
||||||
@ -97,34 +95,19 @@ export const RecordShowPage = () => {
|
|||||||
return [updateEntity, { loading: false }];
|
return [updateEntity, { loading: false }];
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFavorite = objectNameSingular
|
const correspondingFavorite = favorites.find(
|
||||||
? favorites.some((favorite) => favorite.recordId === record?.id)
|
(favorite) => favorite.recordId === objectRecordId,
|
||||||
: false;
|
);
|
||||||
|
|
||||||
|
const isFavorite = isDefined(correspondingFavorite);
|
||||||
|
|
||||||
const handleFavoriteButtonClick = async () => {
|
const handleFavoriteButtonClick = async () => {
|
||||||
if (!objectNameSingular || !record) return;
|
if (!objectNameSingular || !record) return;
|
||||||
if (isFavorite) deleteFavorite(record?.id);
|
|
||||||
else {
|
if (isFavorite && record) {
|
||||||
const additionalData =
|
deleteFavorite(correspondingFavorite.id);
|
||||||
objectNameSingular === 'person'
|
} else {
|
||||||
? {
|
createFavorite(record, objectNameSingular);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const getRecordOptimisticEffectDefinition = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletedRecordIds) {
|
if (isNonEmptyArray(deletedRecordIds)) {
|
||||||
draft.edges = draft.edges.filter(
|
draft.edges = draft.edges.filter(
|
||||||
(edge) => !deletedRecordIds.includes(edge.node.id),
|
(edge) => !deletedRecordIds.includes(edge.node.id),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMeta
|
|||||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
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,
|
objectNameSingular,
|
||||||
}: ObjectMetadataItemIdentifier) => {
|
}: ObjectMetadataItemIdentifier) => {
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||||
@ -62,16 +64,16 @@ export const useCreateManyRecords = <T extends Record<string, unknown>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createdRecords =
|
const createdRecords =
|
||||||
(createdObjects.data[
|
createdObjects.data[
|
||||||
`create${capitalize(objectMetadataItem.namePlural)}`
|
`create${capitalize(objectMetadataItem.namePlural)}`
|
||||||
] as T[]) ?? [];
|
] ?? [];
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
triggerOptimisticEffects({
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||||
createdRecords,
|
createdRecords,
|
||||||
});
|
});
|
||||||
|
|
||||||
return createdRecords;
|
return createdRecords as T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createManyRecords };
|
return { createManyRecords };
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { v4 } from 'uuid';
|
|||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||||
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useCreateOneRecordProps = {
|
type useCreateOneRecordProps = {
|
||||||
@ -39,17 +40,20 @@ export const useCreateOneRecord = <T>({
|
|||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (generatedEmptyRecord) {
|
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||||
triggerOptimisticEffects({
|
objectMetadataItem,
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
recordInput: input,
|
||||||
createdRecords: [generatedEmptyRecord],
|
});
|
||||||
});
|
|
||||||
}
|
triggerOptimisticEffects({
|
||||||
|
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||||
|
createdRecords: [generatedEmptyRecord],
|
||||||
|
});
|
||||||
|
|
||||||
const createdObject = await apolloClient.mutate({
|
const createdObject = await apolloClient.mutate({
|
||||||
mutation: createOneRecordMutation,
|
mutation: createOneRecordMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: { id: recordId, ...input },
|
input: { id: recordId, ...sanitizedUpdateOneRecordInput },
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
||||||
|
|||||||
@ -79,9 +79,9 @@ export const useFindManyRecords = <
|
|||||||
>(findManyRecordsQuery, {
|
>(findManyRecordsQuery, {
|
||||||
skip: skip || !objectMetadataItem || !currentWorkspace,
|
skip: skip || !objectMetadataItem || !currentWorkspace,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter ?? {},
|
filter,
|
||||||
limit: limit,
|
limit,
|
||||||
orderBy: orderBy ?? {},
|
orderBy,
|
||||||
},
|
},
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
onCompleted?.(data[objectMetadataItem.namePlural]);
|
onCompleted?.(data[objectMetadataItem.namePlural]);
|
||||||
@ -116,8 +116,8 @@ export const useFindManyRecords = <
|
|||||||
try {
|
try {
|
||||||
await fetchMore({
|
await fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter ?? {},
|
filter,
|
||||||
orderBy: orderBy ?? {},
|
orderBy,
|
||||||
lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined,
|
lastCursor: isNonEmptyString(lastCursor) ? lastCursor : undefined,
|
||||||
},
|
},
|
||||||
updateQuery: (prev, { fetchMoreResult }) => {
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
|
|||||||
@ -18,7 +18,8 @@ export const useGenerateEmptyRecord = ({
|
|||||||
validatedInput[fieldMetadataItem.name] ??
|
validatedInput[fieldMetadataItem.name] ??
|
||||||
generateEmptyFieldValue(fieldMetadataItem);
|
generateEmptyFieldValue(fieldMetadataItem);
|
||||||
}
|
}
|
||||||
return emptyRecord as T;
|
|
||||||
|
return emptyRecord;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
|||||||
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
|
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
|
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
|
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
@ -50,6 +51,8 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
||||||
|
|
||||||
const objectMetadataType =
|
const objectMetadataType =
|
||||||
objectNameSingular === 'company'
|
objectNameSingular === 'company'
|
||||||
? 'Company'
|
? 'Company'
|
||||||
@ -57,10 +60,6 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
? 'Person'
|
? 'Person'
|
||||||
: 'Custom';
|
: 'Custom';
|
||||||
|
|
||||||
const { createFavorite, deleteFavorite, favorites } = useFavorites({
|
|
||||||
objectNamePlural,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
|
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
|
||||||
const selectedRowIds = snapshot
|
const selectedRowIds = snapshot
|
||||||
.getLoadable(selectedRowIdsSelector)
|
.getLoadable(selectedRowIdsSelector)
|
||||||
@ -68,16 +67,22 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
|
|
||||||
const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : '';
|
const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : '';
|
||||||
|
|
||||||
const isFavorite =
|
const selectedRecord = snapshot
|
||||||
!!selectedRowId &&
|
.getLoadable(entityFieldsFamilyState(selectedRowId))
|
||||||
!!favorites?.find((favorite) => favorite.recordId === selectedRowId);
|
.getValue();
|
||||||
|
|
||||||
|
const foundFavorite = favorites?.find(
|
||||||
|
(favorite) => favorite.recordId === selectedRowId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFavorite = !!selectedRowId && !!foundFavorite;
|
||||||
|
|
||||||
resetTableRowSelection();
|
resetTableRowSelection();
|
||||||
|
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
deleteFavorite(selectedRowId);
|
deleteFavorite(foundFavorite.id);
|
||||||
} else {
|
} else if (selectedRecord) {
|
||||||
createFavorite(selectedRowId);
|
createFavorite(selectedRecord, objectNameSingular);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client';
|
|||||||
|
|
||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
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';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useUpdateOneRecordProps = {
|
type useUpdateOneRecordProps = {
|
||||||
@ -37,17 +37,10 @@ export const useUpdateOneRecord = <T>({
|
|||||||
...updateOneRecordInput,
|
...updateOneRecordInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizedUpdateOneRecordInput = Object.fromEntries(
|
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||||
Object.keys(updateOneRecordInput)
|
objectMetadataItem,
|
||||||
.filter((fieldName) => {
|
recordInput: updateOneRecordInput,
|
||||||
const fieldDefinition = objectMetadataItem.fields.find(
|
});
|
||||||
(field) => field.name === fieldName,
|
|
||||||
);
|
|
||||||
|
|
||||||
return fieldDefinition?.type !== FieldMetadataType.Relation;
|
|
||||||
})
|
|
||||||
.map((fieldName) => [fieldName, updateOneRecordInput[fieldName]]),
|
|
||||||
);
|
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
triggerOptimisticEffects({
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||||
|
|||||||
@ -5,4 +5,5 @@ export type ObjectRecordIdentifier = {
|
|||||||
name: string;
|
name: string;
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
avatarType?: AvatarType | null;
|
avatarType?: AvatarType | null;
|
||||||
|
linkToShowPage?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user