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 <charlesBochet@users.noreply.github.com>
This commit is contained in:
Lucas Bordeau
2023-11-17 19:10:33 +01:00
committed by GitHub
parent 900c863f02
commit 50d6ab52d7
11 changed files with 166 additions and 127 deletions

View File

@ -25,7 +25,11 @@ import { optimisticEffectState } from '../states/optimisticEffectState';
import { OptimisticEffect } from '../types/internal/OptimisticEffect'; import { OptimisticEffect } from '../types/internal/OptimisticEffect';
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition'; import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
export const useOptimisticEffect = (objectNameSingular?: string) => { export const useOptimisticEffect = ({
objectNameSingular,
}: {
objectNameSingular: string | undefined;
}) => {
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const { findManyQuery } = useFindOneObjectMetadataItem({ const { findManyQuery } = useFindOneObjectMetadataItem({
objectNameSingular, objectNameSingular,

View File

@ -1,18 +1,10 @@
import { useState } from 'react';
import styled from '@emotion/styled'; 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 { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import NavItem from '@/ui/navigation/navbar/components/NavItem'; import NavItem from '@/ui/navigation/navbar/components/NavItem';
import NavTitle from '@/ui/navigation/navbar/components/NavTitle'; import NavTitle from '@/ui/navigation/navbar/components/NavTitle';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { Company, Favorite } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useFavorites } from '../hooks/useFavorites'; import { useFavorites } from '../hooks/useFavorites';
@ -28,73 +20,6 @@ export const Favorites = () => {
const { favorites, handleReorderFavorite } = useFavorites({ const { favorites, handleReorderFavorite } = useFavorites({
objectNamePlural: 'companiesV2', objectNamePlural: 'companiesV2',
}); });
const [allCompanies, setAllCompanies] = useState<
Record<string, { name: string; domainName?: string }>
>({});
const [allPeople, setAllPeople] = useState<
Record<string, { firstName: string; lastName: string; avatarUrl?: string }>
>({});
// 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 <></>; if (!favorites || favorites.length === 0) return <></>;

View File

@ -1,10 +1,16 @@
import { useState } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { OnDragEndResponder } from '@hello-pangea/dnd'; import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
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 { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem'; 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'; import { favoritesState } from '../states/favoritesState';
@ -29,11 +35,81 @@ export const useFavorites = ({
const apolloClient = useApolloClient(); const apolloClient = useApolloClient();
const [allCompanies, setAllCompanies] = useState<
Record<string, { name: string; domainName?: string }>
>({});
const [allPeople, setAllPeople] = useState<
Record<string, { firstName: string; lastName: string; avatarUrl?: string }>
>({});
// 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 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( const createFavorite = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
async (favoriteTargetObjectId: string, additionalData?: any) => { async (favoriteTargetObjectId: string, additionalData?: any) => {
const favoritesStateFromSnapshot = snapshot.getLoadable(favoritesState); const favorites = snapshot.getLoadable(favoritesState).getValue();
const favorites = favoritesStateFromSnapshot.getValue();
const targetObjectName = const targetObjectName =
favoriteTargetObjectMetadataItem?.nameSingular.replace('V2', '') ?? favoriteTargetObjectMetadataItem?.nameSingular.replace('V2', '') ??
@ -54,14 +130,21 @@ export const useFavorites = ({
const newFavorite = { const newFavorite = {
...additionalData, ...additionalData,
...createdFavorite[targetObjectName], ...createdFavorite,
}; };
const newFavoritesMapped = mapFavorites([newFavorite], {
...allCompanies,
...allPeople,
});
if (createdFavorite) { if (createdFavorite) {
set(favoritesState, [...favorites, newFavorite]); set(favoritesState, [...favorites, ...newFavoritesMapped]);
} }
}, },
[ [
allCompanies,
allPeople,
apolloClient, apolloClient,
createOneMutation, createOneMutation,
currentWorkspaceMember, currentWorkspaceMember,

View File

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

View File

@ -9,7 +9,9 @@ import { capitalize } from '~/utils/string/capitalize';
export const useCreateOneObjectRecord = <T>({ export const useCreateOneObjectRecord = <T>({
objectNameSingular, objectNameSingular,
}: Pick<ObjectMetadataItemIdentifier, 'objectNameSingular'>) => { }: Pick<ObjectMetadataItemIdentifier, 'objectNameSingular'>) => {
const { triggerOptimisticEffects } = useOptimisticEffect(objectNameSingular); const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { const {
foundObjectMetadataItem, foundObjectMetadataItem,

View File

@ -14,7 +14,9 @@ import { useFindManyObjectRecords } from './useFindManyObjectRecords';
export const useObjectRecordTable = () => { export const useObjectRecordTable = () => {
const { scopeId: objectNamePlural } = useRecordTable(); const { scopeId: objectNamePlural } = useRecordTable();
const { registerOptimisticEffect } = useOptimisticEffect('CompanyV2'); const { registerOptimisticEffect } = useOptimisticEffect({
objectNameSingular: 'companyV2',
});
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({ const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNamePlural, objectNamePlural,

View File

@ -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 { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteOneObjectRecord } from '@/object-record/hooks/useDeleteOneObjectRecord'; 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 { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector'; import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { useGetFavoritesQuery } from '~/generated/graphql';
// TODO: refactor this
export const useRecordTableContextMenuEntries = () => { export const useRecordTableContextMenuEntries = () => {
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
const setTableRowIds = useSetRecoilState(tableRowIdsState); const setTableRowIds = useSetRecoilState(tableRowIdsState);
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
const { scopeId: objectNamePlural, resetTableRowSelection } = const { scopeId: objectNamePlural, resetTableRowSelection } =
useRecordTable(); useRecordTable();
const { data } = useGetFavoritesQuery(); const { createFavorite, deleteFavorite, favorites } = useFavorites({
const favorites = data?.findFavorites; objectNamePlural,
});
const { createFavorite, deleteFavorite } = useFavorites({ objectNamePlural });
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => { const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = snapshot const selectedRowIds = snapshot
@ -39,7 +41,7 @@ export const useRecordTableContextMenuEntries = () => {
const isFavorite = const isFavorite =
!!selectedRowId && !!selectedRowId &&
!!favorites?.find((favorite) => favorite.company?.id === selectedRowId); !!favorites?.find((favorite) => favorite.recordId === selectedRowId);
resetTableRowSelection(); resetTableRowSelection();
@ -73,19 +75,15 @@ export const useRecordTableContextMenuEntries = () => {
}); });
return { return {
setContextMenuEntries: useRecoilCallback(({ snapshot }) => () => { setContextMenuEntries: useCallback(() => {
const selectedRowIds = snapshot
.getLoadable(selectedRowIdsSelector)
.getValue();
const selectedRowId = const selectedRowId =
selectedRowIds.length === 1 ? selectedRowIds[0] : ''; selectedRowIds.length === 1 ? selectedRowIds[0] : '';
const isFavorite = const isFavorite =
!!selectedRowId && isNonEmptyString(selectedRowId) &&
!!favorites?.find((favorite) => favorite.company?.id === selectedRowId); !!favorites?.find((favorite) => favorite.recordId === selectedRowId);
setContextMenuEntries([ const contextMenuEntries = [
{ {
label: 'New task', label: 'New task',
Icon: IconCheckbox, Icon: IconCheckbox,
@ -107,8 +105,21 @@ export const useRecordTableContextMenuEntries = () => {
accent: 'danger', accent: 'danger',
onClick: () => handleDeleteClick(), onClick: () => handleDeleteClick(),
}, },
]); ];
}),
if (selectedRowIds.length > 1) {
contextMenuEntries.splice(2, 1);
}
setContextMenuEntries(contextMenuEntries as any);
}, [
selectedRowIds,
favorites,
handleDeleteClick,
handleFavoriteButtonClick,
setContextMenuEntries,
]),
setActionBarEntries: useRecoilCallback(() => () => { setActionBarEntries: useRecoilCallback(() => () => {
setActionBarEntriesState([ setActionBarEntriesState([
{ {

View File

@ -35,7 +35,10 @@ export const RecordTableEffect = ({
const { setRecordTableData } = useRecordTable(); const { setRecordTableData } = useRecordTable();
const { tableSortsOrderBySelector, tableFiltersWhereSelector } = const { tableSortsOrderBySelector, tableFiltersWhereSelector } =
useRecordTableScopedStates(); useRecordTableScopedStates();
const { registerOptimisticEffect } = useOptimisticEffect('CompanyV2');
const { registerOptimisticEffect } = useOptimisticEffect({
objectNameSingular: 'companyV2',
});
const tableSortsOrderBy = useRecoilValue(tableSortsOrderBySelector); const tableSortsOrderBy = useRecoilValue(tableSortsOrderBySelector);
const sortsOrderBy = defaults(tableSortsOrderBy, [ const sortsOrderBy = defaults(tableSortsOrderBy, [

View File

@ -29,7 +29,9 @@ export const Companies = () => {
recordTableScopeId: 'companies', recordTableScopeId: 'companies',
}); });
const upsertTableRowIds = useUpsertTableRowId(); const upsertTableRowIds = useUpsertTableRowId();
const { triggerOptimisticEffects } = useOptimisticEffect('Company'); const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular: 'company',
});
const handleAddButtonClick = async () => { const handleAddButtonClick = async () => {
const newCompanyId: string = v4(); const newCompanyId: string = v4();

View File

@ -27,7 +27,9 @@ export const People = () => {
recordTableScopeId: 'people', recordTableScopeId: 'people',
}); });
const upsertTableRowIds = useUpsertTableRowId(); const upsertTableRowIds = useUpsertTableRowId();
const { triggerOptimisticEffects } = useOptimisticEffect('Person'); const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular: 'Person',
});
const handleAddButtonClick = async () => { const handleAddButtonClick = async () => {
const newPersonId: string = v4(); const newPersonId: string = v4();

View File

@ -42,6 +42,7 @@ const StyledH1Title = styled(H1Title)`
export const SettingsDevelopersApiKeys = () => { export const SettingsDevelopersApiKeys = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [apiKeys, setApiKeys] = useState<Array<ApiFieldItem>>([]); const [apiKeys, setApiKeys] = useState<Array<ApiFieldItem>>([]);
const { registerOptimisticEffect } = useOptimisticEffect('apiKeyV2'); const { registerOptimisticEffect } = useOptimisticEffect('apiKeyV2');
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({ const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({