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:
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ export const getRecordOptimisticEffectDefinition = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedRecordIds) {
|
||||
if (isNonEmptyArray(deletedRecordIds)) {
|
||||
draft.edges = draft.edges.filter(
|
||||
(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 { 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 };
|
||||
|
||||
@ -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)}`]:
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -18,7 +18,8 @@ export const useGenerateEmptyRecord = ({
|
||||
validatedInput[fieldMetadataItem.name] ??
|
||||
generateEmptyFieldValue(fieldMetadataItem);
|
||||
}
|
||||
return emptyRecord as T;
|
||||
|
||||
return emptyRecord;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -5,4 +5,5 @@ export type ObjectRecordIdentifier = {
|
||||
name: string;
|
||||
avatarUrl?: string | 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