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
18 changed files with 317 additions and 299 deletions

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) {
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)}`]:

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