Update favorites query and state to work with new backend (#2520)

* wip

* wip

* adding favorite works in the database

* favorites are showing in the left drawer

* update favoorite NavItem link

* wip

* adding favorite works

* everything seems to work

* fix delete bug

* fix update favorite position

* update Favorite type

* Fix

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
bosiraphael
2023-11-15 16:17:50 +01:00
committed by GitHub
parent f49ddec2f6
commit 1fc3124d1e
14 changed files with 317 additions and 192 deletions

View File

@ -38,8 +38,7 @@ export const useAuth = () => {
const [verify] = useVerifyMutation();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
const [getCurrentWorkspaceQuery, { data: getCurrentWorkspaceData }] =
useGetCurrentWorkspaceLazyQuery();
const [getCurrentWorkspaceQuery] = useGetCurrentWorkspaceLazyQuery();
const client = useApolloClient();
@ -185,7 +184,13 @@ export const useAuth = () => {
return { user, workspaceMember, workspace };
},
[setIsVerifyPendingState, signUp, handleVerify, client],
[
setIsVerifyPendingState,
signUp,
handleVerify,
client,
setCurrentWorkspaceMember,
],
);
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {

View File

@ -36,7 +36,7 @@ export const useCompanyTableContextMenuEntries = () => {
const { data } = useGetFavoritesQuery();
const favorites = data?.findFavorites;
const { insertCompanyFavorite, deleteCompanyFavorite } = useFavorites();
const { createFavorite, deleteFavorite } = useFavorites();
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = snapshot
@ -53,8 +53,8 @@ export const useCompanyTableContextMenuEntries = () => {
);
resetTableRowSelection();
if (isFavorite) deleteCompanyFavorite(selectedCompanyId);
else insertCompanyFavorite(selectedCompanyId);
if (isFavorite) deleteFavorite(selectedCompanyId);
else createFavorite('company', selectedCompanyId);
});
const [deleteManyCompany] = useDeleteManyCompaniesMutation({

View File

@ -1,16 +1,20 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { favoritesState } from '@/favorites/states/favoritesState';
import { mapFavorites } from '@/favorites/utils/mapFavorites';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import NavItem from '@/ui/navigation/navbar/components/NavItem';
import NavTitle from '@/ui/navigation/navbar/components/NavTitle';
import { Avatar } from '@/users/components/Avatar';
import { useGetFavoritesQuery } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { Company, Favorite } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useFavorites } from '../hooks/useFavorites';
import { favoritesState } from '../states/favoritesState';
const StyledContainer = styled.div`
display: flex;
@ -20,34 +24,73 @@ const StyledContainer = styled.div`
`;
export const Favorites = () => {
const [favorites, setFavorites] = useRecoilState(favoritesState);
const { handleReorderFavorite } = useFavorites();
const { favorites, handleReorderFavorite } = useFavorites();
const [allCompanies, setAllCompanies] = useState<
Record<string, { name: string; domainName?: string }>
>({});
const [allPeople, setAllPeople] = useState<
Record<string, { firstName: string; lastName: string; avatarUrl?: string }>
>({});
useGetFavoritesQuery({
onCompleted: (data) =>
setFavorites(
data?.findFavorites.map((favorite) => {
return {
id: favorite.id,
person: favorite.person
? {
id: favorite.person.id,
firstName: favorite.person.firstName,
lastName: favorite.person.lastName,
avatarUrl: favorite.person.avatarUrl,
}
: undefined,
company: favorite.company
? {
id: favorite.company.id,
name: favorite.company.name,
domainName: favorite.company.domainName,
}
: undefined,
position: favorite.position,
};
}) ?? [],
),
// This is only temporary and will be refactored once we have main identifiers
const { loading: companiesLoading } = useFindManyObjectRecords({
objectNamePlural: 'companiesV2',
onCompleted: async (
data: PaginatedObjectTypeResults<Required<Company>>,
) => {
setAllCompanies(
data.edges.reduce(
(acc, { node: company }) => ({
...acc,
[company.id]: {
name: company.name,
domainName: company.domainName,
},
}),
{},
),
);
},
});
const { loading: peopleLoading } = useFindManyObjectRecords({
objectNamePlural: 'peopleV2',
onCompleted: async (data) => {
setAllPeople(
data.edges.reduce(
(acc, { node: person }) => ({
...acc,
[person.id]: {
firstName: person.firstName,
lastName: person.lastName,
avatarUrl: person.avatarUrl,
},
}),
{},
),
);
},
});
useFindManyObjectRecords({
skip: companiesLoading || peopleLoading,
objectNamePlural: 'favoritesV2',
onCompleted: useRecoilCallback(
({ snapshot, set }) =>
async (data: PaginatedObjectTypeResults<Required<Favorite>>) => {
const favoriteState = snapshot.getLoadable(favoritesState);
const favorites = favoriteState.getValue();
const queriedFavorites = mapFavorites(data.edges, {
...allCompanies,
...allPeople,
});
if (!isDeeplyEqual(favorites, queriedFavorites)) {
set(favoritesState, queriedFavorites);
}
},
[allCompanies, allPeople],
),
});
if (!favorites || favorites.length === 0) return <></>;
@ -60,47 +103,28 @@ export const Favorites = () => {
draggableItems={
<>
{favorites.map((favorite, index) => {
const { id, person, company } = favorite;
const { id, labelIdentifier, avatarUrl, avatarType, link } =
favorite;
return (
<DraggableItem
key={id}
draggableId={id}
index={index}
itemComponent={
<>
{person && (
<NavItem
key={id}
label={`${person.firstName} ${person.lastName}`}
Icon={() => (
<Avatar
colorId={person.id}
avatarUrl={person.avatarUrl ?? ''}
type="rounded"
placeholder={`${person.firstName} ${person.lastName}`}
/>
)}
to={`/person/${person.id}`}
<NavItem
key={id}
label={labelIdentifier}
Icon={() => (
<Avatar
colorId={id}
avatarUrl={avatarUrl}
type={avatarType}
placeholder={labelIdentifier}
/>
)}
{company && (
<NavItem
key={id}
label={company.name}
Icon={() => (
<Avatar
avatarUrl={
getLogoUrlFromDomainName(company.domainName) ??
''
}
type="squared"
placeholder={company.name}
/>
)}
to={`/companies/${company.id}`}
/>
)}
</>
to={link}
/>
}
/>
);

View File

@ -1,108 +1,114 @@
import { getOperationName } from '@apollo/client/utilities';
import { useApolloClient } from '@apollo/client';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { GET_COMPANY } from '@/companies/graphql/queries/getCompany';
import { GET_PERSON } from '@/people/graphql/queries/getPerson';
import {
Favorite,
useDeleteFavoriteMutation,
useInsertCompanyFavoriteMutation,
useInsertPersonFavoriteMutation,
useUpdateOneFavoriteMutation,
} from '~/generated/graphql';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Favorite } from '@/favorites/types/Favorite';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { GET_FAVORITES } from '../graphql/queries/getFavorites';
import { favoritesState } from '../states/favoritesState';
export const useFavorites = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [favorites, setFavorites] = useRecoilState(favoritesState);
const [insertCompanyFavoriteMutation] = useInsertCompanyFavoriteMutation();
const [insertPersonFavoriteMutation] = useInsertPersonFavoriteMutation();
const [deleteFavoriteMutation] = useDeleteFavoriteMutation();
const [updateOneFavoritesMutation] = useUpdateOneFavoriteMutation();
const insertCompanyFavorite = (companyId: string) => {
insertCompanyFavoriteMutation({
variables: {
data: {
companyId,
position: favorites.length + 1,
},
},
refetchQueries: [
getOperationName(GET_FAVORITES) ?? '',
getOperationName(GET_COMPANY) ?? '',
],
const { updateOneMutation, createOneMutation, deleteOneMutation } =
useFindOneObjectMetadataItem({
objectNameSingular: 'favoriteV2',
});
};
const insertPersonFavorite = (personId: string) => {
insertPersonFavoriteMutation({
variables: {
data: {
personId,
position: favorites.length + 1,
},
},
refetchQueries: [
getOperationName(GET_FAVORITES) ?? '',
getOperationName(GET_PERSON) ?? '',
],
});
};
const apolloClient = useApolloClient();
const updateFavoritePosition = async (
favorites: Pick<Favorite, 'id' | 'position'>,
) => {
await updateOneFavoritesMutation({
variables: {
data: {
position: favorites?.position,
},
where: {
id: favorites.id,
},
},
refetchQueries: [
getOperationName(GET_FAVORITES) ?? '',
getOperationName(GET_PERSON) ?? '',
getOperationName(GET_COMPANY) ?? '',
],
});
};
const deleteCompanyFavorite = (companyId: string) => {
deleteFavoriteMutation({
variables: {
where: {
companyId: {
equals: companyId,
const createFavorite = useRecoilCallback(
({ snapshot, set }) =>
async (
favoriteNameToCreate: string,
favoriteIdToCreate: string,
additionalData?: any,
) => {
const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState);
const favorites = favoritesStateFromSnapshot.getValue();
if (!favoriteNameToCreate || !favoriteIdToCreate) {
return;
}
const result = await apolloClient.mutate({
mutation: createOneMutation,
variables: {
input: {
[favoriteNameToCreate]: favoriteIdToCreate,
position: favorites.length + 1,
workspaceMember: currentWorkspaceMember?.id,
},
},
},
},
refetchQueries: [
getOperationName(GET_FAVORITES) ?? '',
getOperationName(GET_COMPANY) ?? '',
],
});
};
});
const deletePersonFavorite = (personId: string) => {
deleteFavoriteMutation({
variables: {
where: {
personId: {
equals: personId,
},
},
const createdFavorite = result?.data?.createFavoriteV2;
const newFavorite = {
...additionalData,
...createdFavorite[favoriteNameToCreate],
};
if (createdFavorite) {
set(favoritesState, [...favorites, newFavorite]);
}
},
refetchQueries: [
getOperationName(GET_FAVORITES) ?? '',
getOperationName(GET_PERSON) ?? '',
],
});
};
[apolloClient, createOneMutation, currentWorkspaceMember?.id],
);
const _updateFavoritePosition = useRecoilCallback(
({ snapshot, set }) =>
async (favoriteToUpdate: Favorite) => {
const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState);
const favorites = favoritesStateFromSnapshot.getValue();
const result = await apolloClient.mutate({
mutation: updateOneMutation,
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, updateOneMutation],
);
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: deleteOneMutation,
variables: {
idToDelete: idToDelete,
},
});
set(
favoritesState,
favorites.filter((favorite: Favorite) => favorite.id !== idToDelete),
);
},
[apolloClient, deleteOneMutation],
);
const computeNewPosition = (destIndex: number, sourceIndex: number) => {
if (destIndex === 0) {
@ -138,13 +144,12 @@ export const useFavorites = () => {
const removedFav = { ...removed, position: newPosition };
reorderFavorites.splice(result.destination.index, 0, removedFav);
setFavorites(reorderFavorites);
updateFavoritePosition(removedFav);
_updateFavoritePosition(removedFav);
};
return {
insertCompanyFavorite,
insertPersonFavorite,
deleteCompanyFavorite,
deletePersonFavorite,
favorites,
createFavorite,
deleteFavorite,
handleReorderFavorite,
};
};

View File

@ -1,16 +1,8 @@
import { atom } from 'recoil';
import { Company, Favorite, Person } from '~/generated/graphql';
import { Favorite } from '@/favorites/types/Favorite';
export const favoritesState = atom<
Array<
Pick<Favorite, 'id' | 'position'> & {
company?: Pick<Company, 'id' | 'name' | 'domainName'>;
} & {
person?: Pick<Person, 'id' | 'firstName' | 'lastName' | 'avatarUrl'>;
}
>
>({
export const favoritesState = atom<Favorite[]>({
key: 'favoritesState',
default: [],
});

View File

@ -0,0 +1,12 @@
import { AvatarType } from '@/users/components/Avatar';
export type Favorite = {
id: string;
position: number;
[key: string]: any;
labelIdentifier: string;
avatarUrl: string;
avatarType: AvatarType;
link: string;
recordId: string;
};

View File

@ -0,0 +1,51 @@
import { Company, Person } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { assertNotNull } from '~/utils/assert';
export const mapFavorites = (
favorites: any,
recordsDict: {
[key: string]: {
firstName?: Person['firstName'];
lastName?: Person['lastName'];
avatarUrl?: Person['avatarUrl'];
name?: Company['name'];
domainName?: Company['domainName'];
};
},
) => {
return favorites
.map(({ node: favorite }: any) => {
const recordInformation = favorite.person
? {
id: favorite.person.id,
labelIdentifier:
recordsDict[favorite.person.id].firstName +
' ' +
recordsDict[favorite.person.id].lastName,
avatarUrl: recordsDict[favorite.person.id].avatarUrl,
avatarType: 'rounded',
link: `/object/personV2/${favorite.person.id}`,
}
: favorite.company
? {
id: favorite.company.id,
labelIdentifier: recordsDict[favorite.company.id].name,
avatarUrl: getLogoUrlFromDomainName(
recordsDict[favorite.company.id].domainName ?? '',
),
avatarType: 'squared',
link: `/object/companyV2/${favorite.company.id}`,
}
: undefined;
return {
...recordInformation,
recordId: recordInformation?.id,
id: favorite.id,
position: favorite.position,
};
})
.filter(assertNotNull)
.sort((a: any, b: any) => a.position - b.position);
};

View File

@ -3,6 +3,7 @@ import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { IconBuildingSkyscraper } from '@/ui/display/icon';
@ -24,6 +25,7 @@ import { PropertyBox } from '@/ui/object/record-inline-cell/property-box/compone
import { InlineCellHotkeyScope } from '@/ui/object/record-inline-cell/types/InlineCellHotkeyScope';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { getLogoUrlFromDomainName } from '~/utils';
import { useFindOneObjectRecord } from '../hooks/useFindOneObjectRecord';
import { useUpdateOneObjectRecord } from '../hooks/useUpdateOneObjectRecord';
@ -34,6 +36,8 @@ export const RecordShowPage = () => {
objectMetadataId: string;
}>();
const { favorites, createFavorite, deleteFavorite } = useFavorites();
const { icons } = useLazyLoadIcons();
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
@ -76,8 +80,38 @@ export const RecordShowPage = () => {
return [updateEntity, { loading: false }];
};
const isFavorite = objectNameSingular
? favorites.some((favorite) => favorite.recordId === object?.id)
: false;
const handleFavoriteButtonClick = async () => {
//
if (!objectNameSingular || !object) return;
if (isFavorite) deleteFavorite(object?.id);
else {
const additionalData =
objectNameSingular === 'peopleV2'
? {
labelIdentifier: object.firstName + ' ' + object.lastName,
avatarUrl: object.avatarUrl,
avatarType: 'rounded',
link: `/object/personV2/${object.id}`,
recordId: object.id,
}
: objectNameSingular === 'companyV2'
? {
labelIdentifier: object.name,
avatarUrl: getLogoUrlFromDomainName(object.domainName ?? ''),
avatarType: 'squared',
link: `/object/companyV2/${object.id}`,
recordId: object.id,
}
: {};
createFavorite(
objectNameSingular.replace('V2', ''),
object.id,
additionalData,
);
}
};
if (!object) return <></>;
@ -91,7 +125,7 @@ export const RecordShowPage = () => {
Icon={IconBuildingSkyscraper}
>
<PageFavoriteButton
isFavorite={false}
isFavorite={isFavorite}
onClick={handleFavoriteButtonClick}
/>
<ShowPageAddButton

View File

@ -36,7 +36,7 @@ export const usePersonTableContextMenuEntries = () => {
const { data } = useGetFavoritesQuery();
const favorites = data?.findFavorites;
const { insertPersonFavorite, deletePersonFavorite } = useFavorites();
const { createFavorite, deleteFavorite } = useFavorites();
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = snapshot
@ -51,8 +51,8 @@ export const usePersonTableContextMenuEntries = () => {
!!favorites?.find((favorite) => favorite.person?.id === selectedPersonId);
resetTableRowSelection();
if (isFavorite) deletePersonFavorite(selectedPersonId);
else insertPersonFavorite(selectedPersonId);
if (isFavorite) deleteFavorite(selectedPersonId);
else createFavorite('person', selectedPersonId);
});
const [deleteManyPerson] = useDeleteManyPersonMutation({

View File

@ -1,5 +1,7 @@
import { useContext } from 'react';
import { RelationFieldDisplay } from '@/ui/object/field/meta-types/display/components/RelationFieldDisplay';
import { FieldContext } from '../contexts/FieldContext';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
@ -10,7 +12,6 @@ import { MoneyAmountV2FieldDisplay } from '../meta-types/display/components/Mone
import { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
import { URLFieldDisplay } from '../meta-types/display/components/URLFieldDisplay';
import { URLV2FieldDisplay } from '../meta-types/display/components/URLV2FieldDisplay';

View File

@ -32,7 +32,7 @@ import { companyShowFieldDefinitions } from './constants/companyShowFieldDefinit
export const CompanyShow = () => {
const companyId = useParams().companyId ?? '';
const { insertCompanyFavorite, deleteCompanyFavorite } = useFavorites();
const { createFavorite, deleteFavorite } = useFavorites();
const navigate = useNavigate();
const { data, loading } = useCompanyQuery(companyId);
const company = data?.findUniqueCompany;
@ -49,8 +49,8 @@ export const CompanyShow = () => {
company.Favorite && company.Favorite?.length > 0 ? true : false;
const handleFavoriteButtonClick = async () => {
if (isFavorite) deleteCompanyFavorite(companyId);
else insertCompanyFavorite(companyId);
if (isFavorite) deleteFavorite(companyId);
else createFavorite('company', companyId);
};
return (

View File

@ -35,7 +35,7 @@ import { personShowFieldDefinition } from './constants/personShowFieldDefinition
export const PersonShow = () => {
const personId = useParams().personId ?? '';
const { insertPersonFavorite, deletePersonFavorite } = useFavorites();
const { createFavorite, deleteFavorite } = useFavorites();
const navigate = useNavigate();
const { data, loading } = usePersonQuery(personId);
@ -68,8 +68,8 @@ export const PersonShow = () => {
};
const handleFavoriteButtonClick = async () => {
if (isFavorite) deletePersonFavorite(personId);
else insertPersonFavorite(personId);
if (isFavorite) deleteFavorite(person.id);
else createFavorite('person', person.id);
};
return (

View File

@ -11,6 +11,7 @@ export enum SeedFavoriteFieldMetadataIds {
WorkspaceMember = '20202020-1138-4e93-bbff-917a68161abf',
Person = '20202020-0876-4735-8974-ff4d51aafa07',
Company = '20202020-09e1-4384-ae3e-39e7956396fe',
CompanyV2 = '20202020-09e1-4384-ae3e-39e7956396ff',
}
export const seedFavoriteFieldMetadata = async (
@ -86,7 +87,7 @@ export const seedFavoriteFieldMetadata = async (
},
description: 'Favorite person',
icon: 'IconUser',
isNullable: false,
isNullable: true,
},
{
id: SeedFavoriteFieldMetadataIds.Company,
@ -102,7 +103,7 @@ export const seedFavoriteFieldMetadata = async (
},
description: 'Favorite company',
icon: 'IconBuildingSkyscraper',
isNullable: false,
isNullable: true,
},
])
.execute();

View File

@ -105,7 +105,7 @@ export const seedPersonFieldMetadata = async (
isCustom: false,
workspaceId: SeedWorkspaceId,
isActive: true,
type: 'URL',
type: 'TEXT',
name: 'linkedinUrl',
label: 'Linkedin',
targetColumnMap: {
@ -121,7 +121,7 @@ export const seedPersonFieldMetadata = async (
isCustom: false,
workspaceId: SeedWorkspaceId,
isActive: true,
type: 'URL',
type: 'TEXT',
name: 'xUrl',
label: 'X',
targetColumnMap: {