From 50d6ab52d7c26c539180bd649c578d096585d190 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 17 Nov 2023 19:10:33 +0100 Subject: [PATCH] Fix favorites add/remove from table context menu (#2571) * Fix favorites add/remove from table context menu * Fixed console.log --------- Co-authored-by: Charles Bochet --- .../hooks/useOptimisticEffect.ts | 6 +- .../favorites/components/Favorites.tsx | 75 --------------- .../modules/favorites/hooks/useFavorites.ts | 91 ++++++++++++++++++- .../modules/favorites/utils/mapFavorites.ts | 54 ++++++----- .../hooks/useCreateOneObjectRecord.ts | 4 +- .../hooks/useObjectRecordTable.ts | 4 +- .../useRecordTableContextMenuEntries.tsx | 45 +++++---- .../components/RecordTableEffect.tsx | 5 +- front/src/pages/companies/Companies.tsx | 4 +- front/src/pages/people/People.tsx | 4 +- .../api-keys/SettingsDevelopersApiKeys.tsx | 1 + 11 files changed, 166 insertions(+), 127 deletions(-) diff --git a/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts index 8245609af..a01a759e8 100644 --- a/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts +++ b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts @@ -25,7 +25,11 @@ import { optimisticEffectState } from '../states/optimisticEffectState'; import { OptimisticEffect } from '../types/internal/OptimisticEffect'; import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition'; -export const useOptimisticEffect = (objectNameSingular?: string) => { +export const useOptimisticEffect = ({ + objectNameSingular, +}: { + objectNameSingular: string | undefined; +}) => { const apolloClient = useApolloClient(); const { findManyQuery } = useFindOneObjectMetadataItem({ objectNameSingular, diff --git a/front/src/modules/favorites/components/Favorites.tsx b/front/src/modules/favorites/components/Favorites.tsx index d179bef83..baa75ff47 100644 --- a/front/src/modules/favorites/components/Favorites.tsx +++ b/front/src/modules/favorites/components/Favorites.tsx @@ -1,18 +1,10 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; -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 { Company, Favorite } from '~/generated-metadata/graphql'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { useFavorites } from '../hooks/useFavorites'; @@ -28,73 +20,6 @@ export const Favorites = () => { const { favorites, handleReorderFavorite } = useFavorites({ objectNamePlural: 'companiesV2', }); - const [allCompanies, setAllCompanies] = useState< - Record - >({}); - const [allPeople, setAllPeople] = useState< - Record - >({}); - - // This is only temporary and will be refactored once we have main identifiers - const { loading: companiesLoading } = useFindManyObjectRecords({ - objectNamePlural: 'companiesV2', - onCompleted: async ( - data: PaginatedObjectTypeResults>, - ) => { - 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>) => { - 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 <>; diff --git a/front/src/modules/favorites/hooks/useFavorites.ts b/front/src/modules/favorites/hooks/useFavorites.ts index bd5b0fdca..696f97bea 100644 --- a/front/src/modules/favorites/hooks/useFavorites.ts +++ b/front/src/modules/favorites/hooks/useFavorites.ts @@ -1,10 +1,16 @@ +import { useState } from 'react'; import { useApolloClient } from '@apollo/client'; import { OnDragEndResponder } from '@hello-pangea/dnd'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { Favorite } from '@/favorites/types/Favorite'; +import { mapFavorites } from '@/favorites/utils/mapFavorites'; import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem'; +import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; +import { PaginatedObjectTypeResults } from '@/object-record/types/PaginatedObjectTypeResults'; +import { Company } from '~/generated/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { favoritesState } from '../states/favoritesState'; @@ -29,11 +35,81 @@ export const useFavorites = ({ const apolloClient = useApolloClient(); + const [allCompanies, setAllCompanies] = useState< + Record + >({}); + const [allPeople, setAllPeople] = useState< + Record + >({}); + + // This is only temporary and will be refactored once we have main identifiers + const { loading: companiesLoading } = useFindManyObjectRecords({ + objectNamePlural: 'companiesV2', + onCompleted: async ( + data: PaginatedObjectTypeResults>, + ) => { + 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>) => { + const favorites = snapshot.getLoadable(favoritesState).getValue(); + + const queriedFavorites = mapFavorites( + data.edges.map((edge) => edge.node), + { + ...allCompanies, + ...allPeople, + }, + ); + + if (!isDeeplyEqual(favorites, queriedFavorites)) { + set(favoritesState, queriedFavorites); + } + }, + [allCompanies, allPeople], + ), + }); + const createFavorite = useRecoilCallback( ({ snapshot, set }) => async (favoriteTargetObjectId: string, additionalData?: any) => { - const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState); - const favorites = favoritesStateFromSnapshot.getValue(); + const favorites = snapshot.getLoadable(favoritesState).getValue(); const targetObjectName = favoriteTargetObjectMetadataItem?.nameSingular.replace('V2', '') ?? @@ -54,14 +130,21 @@ export const useFavorites = ({ const newFavorite = { ...additionalData, - ...createdFavorite[targetObjectName], + ...createdFavorite, }; + const newFavoritesMapped = mapFavorites([newFavorite], { + ...allCompanies, + ...allPeople, + }); + if (createdFavorite) { - set(favoritesState, [...favorites, newFavorite]); + set(favoritesState, [...favorites, ...newFavoritesMapped]); } }, [ + allCompanies, + allPeople, apolloClient, createOneMutation, currentWorkspaceMember, diff --git a/front/src/modules/favorites/utils/mapFavorites.ts b/front/src/modules/favorites/utils/mapFavorites.ts index 309f93aec..7810ae26d 100644 --- a/front/src/modules/favorites/utils/mapFavorites.ts +++ b/front/src/modules/favorites/utils/mapFavorites.ts @@ -1,6 +1,7 @@ import { Company, Person } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; import { assertNotNull } from '~/utils/assert'; +import { isDefined } from '~/utils/isDefined'; export const mapFavorites = ( favorites: any, @@ -15,35 +16,38 @@ export const mapFavorites = ( }, ) => { 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; + .map((favorite: any) => { + const recordInformation = + isDefined(favorite?.person) && + isDefined(recordsDict[favorite.person.id]) + ? { + 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}`, + } + : isDefined(favorite?.company) && + isDefined(recordsDict[favorite.company.id]) + ? { + 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, + id: favorite?.id, + position: favorite?.position, }; }) .filter(assertNotNull) diff --git a/front/src/modules/object-record/hooks/useCreateOneObjectRecord.ts b/front/src/modules/object-record/hooks/useCreateOneObjectRecord.ts index 865b3d4d8..f15e3b9bc 100644 --- a/front/src/modules/object-record/hooks/useCreateOneObjectRecord.ts +++ b/front/src/modules/object-record/hooks/useCreateOneObjectRecord.ts @@ -9,7 +9,9 @@ import { capitalize } from '~/utils/string/capitalize'; export const useCreateOneObjectRecord = ({ objectNameSingular, }: Pick) => { - const { triggerOptimisticEffects } = useOptimisticEffect(objectNameSingular); + const { triggerOptimisticEffects } = useOptimisticEffect({ + objectNameSingular, + }); const { foundObjectMetadataItem, diff --git a/front/src/modules/object-record/hooks/useObjectRecordTable.ts b/front/src/modules/object-record/hooks/useObjectRecordTable.ts index c7d0dc211..edf321409 100644 --- a/front/src/modules/object-record/hooks/useObjectRecordTable.ts +++ b/front/src/modules/object-record/hooks/useObjectRecordTable.ts @@ -14,7 +14,9 @@ import { useFindManyObjectRecords } from './useFindManyObjectRecords'; export const useObjectRecordTable = () => { const { scopeId: objectNamePlural } = useRecordTable(); - const { registerOptimisticEffect } = useOptimisticEffect('CompanyV2'); + const { registerOptimisticEffect } = useOptimisticEffect({ + objectNameSingular: 'companyV2', + }); const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({ objectNamePlural, diff --git a/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx b/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx index e26961f00..64a17da89 100644 --- a/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx +++ b/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx @@ -1,4 +1,6 @@ -import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { useCallback } from 'react'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord'; @@ -14,21 +16,21 @@ import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/con import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector'; import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; -import { useGetFavoritesQuery } from '~/generated/graphql'; +// TODO: refactor this export const useRecordTableContextMenuEntries = () => { const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); const setTableRowIds = useSetRecoilState(tableRowIdsState); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); const { scopeId: objectNamePlural, resetTableRowSelection } = useRecordTable(); - const { data } = useGetFavoritesQuery(); - const favorites = data?.findFavorites; - - const { createFavorite, deleteFavorite } = useFavorites({ objectNamePlural }); + const { createFavorite, deleteFavorite, favorites } = useFavorites({ + objectNamePlural, + }); const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => { const selectedRowIds = snapshot @@ -39,7 +41,7 @@ export const useRecordTableContextMenuEntries = () => { const isFavorite = !!selectedRowId && - !!favorites?.find((favorite) => favorite.company?.id === selectedRowId); + !!favorites?.find((favorite) => favorite.recordId === selectedRowId); resetTableRowSelection(); @@ -73,19 +75,15 @@ export const useRecordTableContextMenuEntries = () => { }); return { - setContextMenuEntries: useRecoilCallback(({ snapshot }) => () => { - const selectedRowIds = snapshot - .getLoadable(selectedRowIdsSelector) - .getValue(); - + setContextMenuEntries: useCallback(() => { const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : ''; const isFavorite = - !!selectedRowId && - !!favorites?.find((favorite) => favorite.company?.id === selectedRowId); + isNonEmptyString(selectedRowId) && + !!favorites?.find((favorite) => favorite.recordId === selectedRowId); - setContextMenuEntries([ + const contextMenuEntries = [ { label: 'New task', Icon: IconCheckbox, @@ -107,8 +105,21 @@ export const useRecordTableContextMenuEntries = () => { accent: 'danger', onClick: () => handleDeleteClick(), }, - ]); - }), + ]; + + if (selectedRowIds.length > 1) { + contextMenuEntries.splice(2, 1); + } + + setContextMenuEntries(contextMenuEntries as any); + }, [ + selectedRowIds, + favorites, + handleDeleteClick, + handleFavoriteButtonClick, + setContextMenuEntries, + ]), + setActionBarEntries: useRecoilCallback(() => () => { setActionBarEntriesState([ { diff --git a/front/src/modules/ui/object/record-table/components/RecordTableEffect.tsx b/front/src/modules/ui/object/record-table/components/RecordTableEffect.tsx index 69633313c..d3f84c94d 100644 --- a/front/src/modules/ui/object/record-table/components/RecordTableEffect.tsx +++ b/front/src/modules/ui/object/record-table/components/RecordTableEffect.tsx @@ -35,7 +35,10 @@ export const RecordTableEffect = ({ const { setRecordTableData } = useRecordTable(); const { tableSortsOrderBySelector, tableFiltersWhereSelector } = useRecordTableScopedStates(); - const { registerOptimisticEffect } = useOptimisticEffect('CompanyV2'); + + const { registerOptimisticEffect } = useOptimisticEffect({ + objectNameSingular: 'companyV2', + }); const tableSortsOrderBy = useRecoilValue(tableSortsOrderBySelector); const sortsOrderBy = defaults(tableSortsOrderBy, [ diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index d785b8ef8..af81aca75 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -29,7 +29,9 @@ export const Companies = () => { recordTableScopeId: 'companies', }); const upsertTableRowIds = useUpsertTableRowId(); - const { triggerOptimisticEffects } = useOptimisticEffect('Company'); + const { triggerOptimisticEffects } = useOptimisticEffect({ + objectNameSingular: 'company', + }); const handleAddButtonClick = async () => { const newCompanyId: string = v4(); diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index f7d511938..5fe8606b2 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -27,7 +27,9 @@ export const People = () => { recordTableScopeId: 'people', }); const upsertTableRowIds = useUpsertTableRowId(); - const { triggerOptimisticEffects } = useOptimisticEffect('Person'); + const { triggerOptimisticEffects } = useOptimisticEffect({ + objectNameSingular: 'Person', + }); const handleAddButtonClick = async () => { const newPersonId: string = v4(); diff --git a/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx index 987ed8742..a11d17138 100644 --- a/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx +++ b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx @@ -42,6 +42,7 @@ const StyledH1Title = styled(H1Title)` export const SettingsDevelopersApiKeys = () => { const navigate = useNavigate(); + const [apiKeys, setApiKeys] = useState>([]); const { registerOptimisticEffect } = useOptimisticEffect('apiKeyV2'); const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({