From 69f48ea3302cb837675be67146b488b74e7b980e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 5 Dec 2023 22:29:27 +0100 Subject: [PATCH] Fix optimistic rendering issues on board and table (#2846) * Fix optimistic rendering issues on board and table * Remove dead code * Improve re-renders of Table * Remove re-renders on board --- .../hooks/useOptimisticEffect.ts | 6 +- .../types/OptimisticEffectResolver.ts | 2 + .../types/internal/OptimisticEffect.ts | 1 + .../command-menu/components/CommandMenu.tsx | 6 +- .../components/RecordTableContainer.tsx | 3 +- .../components/RecordTablePage.tsx | 3 +- .../getRecordOptimisticEffectDefinition.ts | 47 +++-- .../object-record/hooks/useCreateOneRecord.ts | 30 ++- .../object-record/hooks/useDeleteOneRecord.ts | 34 +++- .../object-record/hooks/useFindManyRecords.ts | 2 +- .../hooks/useGenerateEmptyRecord.ts | 177 ++++++++++++++++++ .../hooks/useObjectRecordBoard.1.ts | 104 ++++++++++ .../hooks/useObjectRecordBoard.ts | 20 +- .../hooks/useObjectRecordTable.ts | 22 +-- .../useRecordTableContextMenuEntries.tsx | 15 +- .../hooks/useSetRecordTableData.ts | 46 ----- .../object-record/hooks/useUpdateOneRecord.ts | 16 +- .../types/PaginatedRecordTypeResults.ts | 1 + .../record-board/components/RecordBoard.tsx | 10 +- .../components/RecordBoardColumn.tsx | 122 +----------- .../components/RecordBoardColumnHeader.tsx | 136 ++++++++++++++ .../components/RecordBoardInternalEffect.tsx | 33 ++-- .../useUpdateCompanyBoardCardIdsInternal.ts | 32 ---- .../useUpdateRecordBoardCardIdsInternal.ts | 106 ----------- .../components/RecordTableBody.tsx | 56 +++--- .../components/RecordTableBodyEffect.tsx | 10 +- .../hooks/internal/useSetRecordTableData.ts | 12 +- .../record-table/hooks/useUpsertTableRowId.ts | 19 -- 28 files changed, 606 insertions(+), 465 deletions(-) create mode 100644 front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts create mode 100644 front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts delete mode 100644 front/src/modules/object-record/hooks/useSetRecordTableData.ts create mode 100644 front/src/modules/ui/object/record-board/components/RecordBoardColumnHeader.tsx delete mode 100644 front/src/modules/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal.ts delete mode 100644 front/src/modules/ui/object/record-board/hooks/internal/useUpdateRecordBoardCardIdsInternal.ts delete mode 100644 front/src/modules/ui/object/record-table/hooks/useUpsertTableRowId.ts diff --git a/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts index 465b83386..3ba46aae3 100644 --- a/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts +++ b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts @@ -49,12 +49,14 @@ export const useOptimisticEffect = ({ const optimisticEffectWriter = ({ cache, newData, + deletedRecordIds, query, variables, objectMetadataItem, }: { cache: ApolloCache; newData: unknown; + deletedRecordIds?: string[]; variables: OperationVariables; query: DocumentNode; isUsingFlexibleBackend?: boolean; @@ -79,6 +81,7 @@ export const useOptimisticEffect = ({ objectMetadataItem.namePlural ], newData, + deletedRecordIds, variables, }), }, @@ -116,7 +119,7 @@ export const useOptimisticEffect = ({ const triggerOptimisticEffects = useRecoilCallback( ({ snapshot }) => - (typename: string, newData: unknown) => { + (typename: string, newData: unknown, deletedRecordIds?: string[]) => { const optimisticEffects = snapshot .getLoadable(optimisticEffectState) .getValue(); @@ -135,6 +138,7 @@ export const useOptimisticEffect = ({ cache: apolloClient.cache, query: optimisticEffect.query ?? ({} as DocumentNode), newData: formattedNewData, + deletedRecordIds, variables: optimisticEffect.variables, isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend, objectMetadataItem: optimisticEffect.objectMetadataItem, diff --git a/front/src/modules/apollo/optimistic-effect/types/OptimisticEffectResolver.ts b/front/src/modules/apollo/optimistic-effect/types/OptimisticEffectResolver.ts index f1d956b4c..d57d090cc 100644 --- a/front/src/modules/apollo/optimistic-effect/types/OptimisticEffectResolver.ts +++ b/front/src/modules/apollo/optimistic-effect/types/OptimisticEffectResolver.ts @@ -3,9 +3,11 @@ import { OperationVariables } from '@apollo/client'; export type OptimisticEffectResolver = ({ currentData, newData, + deletedRecordIds, variables, }: { currentData: any; //TODO: Change when decommissioning v1 newData: any; //TODO: Change when decommissioning v1 + deletedRecordIds?: string[]; variables: OperationVariables; }) => void; diff --git a/front/src/modules/apollo/optimistic-effect/types/internal/OptimisticEffect.ts b/front/src/modules/apollo/optimistic-effect/types/internal/OptimisticEffect.ts index c18b8d831..8cee693fe 100644 --- a/front/src/modules/apollo/optimistic-effect/types/internal/OptimisticEffect.ts +++ b/front/src/modules/apollo/optimistic-effect/types/internal/OptimisticEffect.ts @@ -11,6 +11,7 @@ type OptimisticEffectWriter = ({ cache: ApolloCache; query: DocumentNode; newData: T; + deletedRecordIds?: string[]; variables: OperationVariables; objectMetadataItem?: ObjectMetadataItem; isUsingFlexibleBackend?: boolean; diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index 0fdda52a1..2efb2233e 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -3,7 +3,9 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; +import { Activity } from '@/activities/types/Activity'; import { CommandMenuSelectableListEffect } from '@/command-menu/components/CommandMenuSelectableListEffect'; +import { Company } from '@/companies/types/Company'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { Person } from '@/people/types/Person'; @@ -130,7 +132,7 @@ export const CommandMenu = () => { limit: 3, }); - const { records: companies } = useFindManyRecords({ + const { records: companies } = useFindManyRecords({ skip: !isCommandMenuOpened, objectNameSingular: 'company', filter: { @@ -139,7 +141,7 @@ export const CommandMenu = () => { limit: 3, }); - const { records: activities } = useFindManyRecords({ + const { records: activities } = useFindManyRecords({ skip: !isCommandMenuOpened, objectNameSingular: 'activity', filter: { diff --git a/front/src/modules/object-record/components/RecordTableContainer.tsx b/front/src/modules/object-record/components/RecordTableContainer.tsx index 1ea6420c4..cf102f842 100644 --- a/front/src/modules/object-record/components/RecordTableContainer.tsx +++ b/front/src/modules/object-record/components/RecordTableContainer.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordTable } from '@/ui/object/record-table/components/RecordTable'; import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId'; import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; @@ -12,8 +13,6 @@ import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToC import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; -import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord'; - import { RecordTableEffect } from './RecordTableEffect'; const StyledContainer = styled.div` diff --git a/front/src/modules/object-record/components/RecordTablePage.tsx b/front/src/modules/object-record/components/RecordTablePage.tsx index c57625a3e..d028edada 100644 --- a/front/src/modules/object-record/components/RecordTablePage.tsx +++ b/front/src/modules/object-record/components/RecordTablePage.tsx @@ -6,6 +6,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { IconBuildingSkyscraper } from '@/ui/display/icon'; import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { PageBody } from '@/ui/layout/page/PageBody'; @@ -15,8 +16,6 @@ import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect'; import { RecordTableActionBar } from '@/ui/object/record-table/action-bar/components/RecordTableActionBar'; import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu'; -import { useCreateOneRecord } from '../hooks/useCreateOneRecord'; - import { RecordTableContainer } from './RecordTableContainer'; const StyledTableContainer = styled.div` diff --git a/front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts b/front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts index a25db475a..4414b3fea 100644 --- a/front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts +++ b/front/src/modules/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition.ts @@ -16,26 +16,49 @@ export const getRecordOptimisticEffectDefinition = ({ resolver: ({ currentData, newData, + deletedRecordIds, }: { currentData: unknown; - newData: unknown; + newData: { id: string } & Record; + deletedRecordIds?: string[]; }) => { const newRecordPaginatedCacheField = produce< PaginatedRecordTypeResults >(currentData as PaginatedRecordTypeResults, (draft) => { - if (!draft) { - return { - edges: [{ node: newData, cursor: '' }], - pageInfo: { - endCursor: '', - hasNextPage: false, - hasPreviousPage: false, - startCursor: '', - }, - }; + if (newData) { + if (!draft) { + return { + __typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, + edges: [{ node: newData, cursor: '' }], + pageInfo: { + endCursor: '', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + }, + }; + } + + const existingRecord = draft.edges.find( + (edge) => edge.node.id === newData.id, + ); + if (existingRecord) { + existingRecord.node = newData; + return; + } + + draft.edges.unshift({ + node: newData, + cursor: '', + __typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`, + }); } - draft.edges.unshift({ node: newData, cursor: '' }); + if (deletedRecordIds) { + draft.edges = draft.edges.filter( + (edge) => !deletedRecordIds.includes(edge.node.id), + ); + } }); return newRecordPaginatedCacheField; diff --git a/front/src/modules/object-record/hooks/useCreateOneRecord.ts b/front/src/modules/object-record/hooks/useCreateOneRecord.ts index 66192b4ef..d8c3ffe24 100644 --- a/front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -1,9 +1,10 @@ -import { useMutation } from '@apollo/client'; +import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord'; import { capitalize } from '~/utils/string/capitalize'; export const useCreateOneRecord = ({ @@ -20,21 +21,42 @@ export const useCreateOneRecord = ({ ); // TODO: type this with a minimal type at least with Record - const [mutate] = useMutation(createOneRecordMutation); + const apolloClient = useApolloClient(); + + const { generateEmptyRecord } = useGenerateEmptyRecord({ + objectMetadataItem, + }); const createOneRecord = async (input: Record) => { - const createdObject = await mutate({ + const recordId = v4(); + + triggerOptimisticEffects( + `${capitalize(objectMetadataItem.nameSingular)}Edge`, + generateEmptyRecord(recordId), + ); + + const createdObject = await apolloClient.mutate({ + mutation: createOneRecordMutation, variables: { - input: { ...input, id: v4() }, + input: { ...input, id: recordId }, + }, + optimisticResponse: { + [`create${capitalize(objectMetadataItem.nameSingular)}`]: + generateEmptyRecord(recordId), }, }); + if (!createdObject.data) { + return null; + } + triggerOptimisticEffects( `${capitalize(objectMetadataItem.nameSingular)}Edge`, createdObject.data[ `create${capitalize(objectMetadataItem.nameSingular)}` ], ); + return createdObject.data[ `create${capitalize(objectMetadataItem.nameSingular)}` ] as T; diff --git a/front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/front/src/modules/object-record/hooks/useDeleteOneRecord.ts index 023e06f02..42b3fcc23 100644 --- a/front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; -import { useMutation } from '@apollo/client'; +import { useApolloClient } from '@apollo/client'; +import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; @@ -10,6 +11,9 @@ export const useDeleteOneRecord = ({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { const { performOptimisticEvict } = useOptimisticEvict(); + const { triggerOptimisticEffects } = useOptimisticEffect({ + objectNameSingular, + }); const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem( { @@ -17,16 +21,15 @@ export const useDeleteOneRecord = ({ }, ); - // TODO: type this with a minimal type at least with Record - const [mutate] = useMutation(deleteOneRecordMutation); + const apolloClient = useApolloClient(); const deleteOneRecord = useCallback( async (idToDelete: string) => { - const deletedRecord = await mutate({ - variables: { - idToDelete, - }, - }); + triggerOptimisticEffects( + `${capitalize(objectMetadataItem.nameSingular)}Edge`, + undefined, + [idToDelete], + ); performOptimisticEvict( capitalize(objectMetadataItem.nameSingular), @@ -34,11 +37,24 @@ export const useDeleteOneRecord = ({ idToDelete, ); + const deletedRecord = await apolloClient.mutate({ + mutation: deleteOneRecordMutation, + variables: { + idToDelete, + }, + }); + return deletedRecord.data[ `create${capitalize(objectMetadataItem.nameSingular)}` ] as T; }, - [performOptimisticEvict, objectMetadataItem, mutate], + [ + triggerOptimisticEffects, + objectMetadataItem.nameSingular, + performOptimisticEvict, + apolloClient, + deleteOneRecordMutation, + ], ); return { diff --git a/front/src/modules/object-record/hooks/useFindManyRecords.ts b/front/src/modules/object-record/hooks/useFindManyRecords.ts index 7aec38ecd..00a2fdad4 100644 --- a/front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -214,7 +214,7 @@ export const useFindManyRecords = < return { objectMetadataItem, - records, + records: records as RecordType[], loading, error, fetchMoreRecords, diff --git a/front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts b/front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts new file mode 100644 index 000000000..e6fd1a709 --- /dev/null +++ b/front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts @@ -0,0 +1,177 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const useGenerateEmptyRecord = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const generateEmptyRecord = (id: string) => { + if (objectMetadataItem.nameSingular === 'company') { + return { + id, + domainName: '', + accountOwnerId: null, + createdAt: '2023-12-05T16:04:42.261Z', + address: '', + people: [ + { + edges: [], + __typename: 'PersonConnection', + }, + ], + xLink: { + label: '', + url: '', + __typename: 'Link', + }, + attachments: { + edges: [], + __typename: 'AttachmentConnection', + }, + activityTargets: { + edges: [], + __typename: 'ActivityTargetConnection', + }, + idealCustomerProfile: null, + annualRecurringRevenue: { + amountMicros: null, + currencyCode: null, + __typename: 'Currency', + }, + updatedAt: '2023-12-05T16:04:42.261Z', + employees: null, + accountOwner: null, + name: '', + linkedinLink: { + label: '', + url: '', + __typename: 'Link', + }, + favorites: { + edges: [], + __typename: 'FavoriteConnection', + }, + opportunities: { + edges: [], + __typename: 'OpportunityConnection', + }, + __typename: 'Company', + }; + } + + if (objectMetadataItem.nameSingular === 'person') { + return { + id, + activityTargets: { + edges: [], + __typename: 'ActivityTargetConnection', + }, + opportunities: { + edges: [], + __typename: 'OpportunityConnection', + }, + companyId: null, + favorites: { + edges: [], + __typename: 'FavoriteConnection', + }, + phone: '', + company: null, + xLink: { + label: '', + url: '', + __typename: 'Link', + }, + jobTitle: '', + pointOfContactForOpportunities: { + edges: [], + __typename: 'OpportunityConnection', + }, + email: '', + attachments: { + edges: [], + __typename: 'AttachmentConnection', + }, + name: { + firstName: '', + lastName: '', + __typename: 'FullName', + }, + avatarUrl: '', + updatedAt: '2023-12-05T16:45:11.840Z', + createdAt: '2023-12-05T16:45:11.840Z', + city: '', + linkedinLink: { + label: '', + url: '', + __typename: 'Link', + }, + __typename: 'Person', + }; + } + + if (objectMetadataItem.nameSingular === 'opportunity') { + return { + id, + pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', + closeDate: null, + companyId: '04b2e9f5-0713-40a5-8216-82802401d33e', + updatedAt: '2023-12-05T16:46:27.621Z', + pipelineStep: { + id: '30b14887-d592-427d-bd97-6e670158db02', + position: 2, + name: 'Meeting', + updatedAt: '2023-12-05T11:29:21.485Z', + createdAt: '2023-12-05T11:29:21.485Z', + color: 'sky', + __typename: 'PipelineStep', + }, + probability: '0', + pointOfContactId: null, + personId: null, + amount: { + amountMicros: null, + currencyCode: null, + __typename: 'Currency', + }, + createdAt: '2023-12-05T16:46:27.621Z', + pointOfContact: null, + person: null, + company: { + id: '04b2e9f5-0713-40a5-8216-82802401d33e', + domainName: 'qonto.com', + accountOwnerId: null, + createdAt: '2023-12-05T11:29:21.484Z', + address: '', + xLink: { + label: '', + url: '', + __typename: 'Link', + }, + idealCustomerProfile: null, + annualRecurringRevenue: { + amountMicros: null, + currencyCode: null, + __typename: 'Currency', + }, + updatedAt: '2023-12-05T11:29:21.484Z', + employees: null, + name: 'Qonto', + linkedinLink: { + label: '', + url: '', + __typename: 'Link', + }, + __typename: 'Company', + }, + __typename: 'Opportunity', + }; + } + + return {}; + }; + + return { + generateEmptyRecord: generateEmptyRecord, + }; +}; diff --git a/front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts b/front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts new file mode 100644 index 000000000..07a4558f2 --- /dev/null +++ b/front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts @@ -0,0 +1,104 @@ +import { useCallback } from 'react'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; + +import { Company } from '@/companies/types/Company'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults'; +import { Opportunity } from '@/pipeline/types/Opportunity'; +import { PipelineStep } from '@/pipeline/types/PipelineStep'; +import { turnFiltersIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause'; +import { turnSortsIntoOrderBy } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; + +import { useFindManyRecords } from './useFindManyRecords'; + +export const useObjectRecordBoard = () => { + const objectNameSingular = 'opportunity'; + + const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem( + { + objectNameSingular, + }, + ); + + const { + isBoardLoadedState, + boardFiltersState, + boardSortsState, + savedCompaniesState, + savedOpportunitiesState, + savedPipelineStepsState, + } = useRecordBoardScopedStates(); + + const setIsBoardLoaded = useSetRecoilState(isBoardLoadedState); + + const boardFilters = useRecoilValue(boardFiltersState); + const boardSorts = useRecoilValue(boardSortsState); + + const setSavedCompanies = useSetRecoilState(savedCompaniesState); + + const [savedOpportunities] = useRecoilState(savedOpportunitiesState); + + const [savedPipelineSteps, setSavedPipelineSteps] = useRecoilState( + savedPipelineStepsState, + ); + + const filter = turnFiltersIntoWhereClause( + boardFilters, + foundObjectMetadataItem?.fields ?? [], + ); + const orderBy = turnSortsIntoOrderBy( + boardSorts, + foundObjectMetadataItem?.fields ?? [], + ); + + useFindManyRecords({ + objectNameSingular: 'pipelineStep', + filter: {}, + onCompleted: useCallback( + (data: PaginatedRecordTypeResults) => { + setSavedPipelineSteps(data.edges.map((edge) => edge.node)); + }, + [setSavedPipelineSteps], + ), + }); + + const { + records: opportunities, + loading, + fetchMoreRecords: fetchMoreOpportunities, + } = useFindManyRecords({ + skip: !savedPipelineSteps.length, + objectNameSingular: 'opportunity', + filter: filter, + orderBy: orderBy, + onCompleted: useCallback(() => { + setIsBoardLoaded(true); + }, [setIsBoardLoaded]), + }); + + const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({ + skip: !savedOpportunities.length, + objectNameSingular: 'company', + filter: { + id: { + in: savedOpportunities.map( + (opportunity) => opportunity.companyId || '', + ), + }, + }, + onCompleted: useCallback( + (data: PaginatedRecordTypeResults) => { + setSavedCompanies(data.edges.map((edge) => edge.node)); + }, + [setSavedCompanies], + ), + }); + + return { + opportunities, + loading, + fetchMoreOpportunities, + fetchMoreCompanies, + }; +}; diff --git a/front/src/modules/object-record/hooks/useObjectRecordBoard.ts b/front/src/modules/object-record/hooks/useObjectRecordBoard.ts index 22274111c..7f0b0f3e3 100644 --- a/front/src/modules/object-record/hooks/useObjectRecordBoard.ts +++ b/front/src/modules/object-record/hooks/useObjectRecordBoard.ts @@ -9,13 +9,11 @@ import { PipelineStep } from '@/pipeline/types/PipelineStep'; import { turnFiltersIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause'; import { turnSortsIntoOrderBy } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; -import { useUpdateCompanyBoardCardIdsInternal } from '@/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal'; import { useFindManyRecords } from './useFindManyRecords'; export const useObjectRecordBoard = () => { const objectNameSingular = 'opportunity'; - const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIdsInternal(); const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem( { @@ -71,24 +69,14 @@ export const useObjectRecordBoard = () => { records: opportunities, loading, fetchMoreRecords: fetchMoreOpportunities, - } = useFindManyRecords({ + } = useFindManyRecords({ skip: !savedPipelineSteps.length, objectNameSingular: 'opportunity', filter: filter, orderBy: orderBy, - onCompleted: useCallback( - (data: PaginatedRecordTypeResults) => { - const pipelineProgresses: Array = data.edges.map( - (edge) => edge.node, - ); - - updateCompanyBoardCardIds(pipelineProgresses); - - setSavedOpportunities(pipelineProgresses); - setIsBoardLoaded(true); - }, - [setIsBoardLoaded, setSavedOpportunities, updateCompanyBoardCardIds], - ), + onCompleted: useCallback(() => { + setIsBoardLoaded(true); + }, [setIsBoardLoaded]), }); const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({ diff --git a/front/src/modules/object-record/hooks/useObjectRecordTable.ts b/front/src/modules/object-record/hooks/useObjectRecordTable.ts index 1a2c6a05d..b2cfd8060 100644 --- a/front/src/modules/object-record/hooks/useObjectRecordTable.ts +++ b/front/src/modules/object-record/hooks/useObjectRecordTable.ts @@ -1,6 +1,5 @@ import { useRecoilValue } from 'recoil'; -import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { turnFiltersIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause'; @@ -8,8 +7,6 @@ import { turnSortsIntoOrderBy } from '@/ui/object/object-sort-dropdown/utils/tur import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates'; import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; -import { getRecordOptimisticEffectDefinition } from '../graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition'; - import { useFindManyRecords } from './useFindManyRecords'; export const useObjectRecordTable = () => { @@ -25,10 +22,6 @@ export const useObjectRecordTable = () => { }, ); - const { registerOptimisticEffect } = useOptimisticEffect({ - objectNameSingular, - }); - const { tableFiltersState, tableSortsState } = useRecordTableScopedStates(); const tableFilters = useRecoilValue(tableFiltersState); @@ -47,25 +40,12 @@ export const useObjectRecordTable = () => { objectNameSingular, filter, orderBy, - onCompleted: (data) => { - const entities = data.edges.map((edge) => edge.node) ?? []; - - setRecordTableData(entities); - - if (foundObjectMetadataItem) { - registerOptimisticEffect({ - variables: { orderBy, filter, limit: 60 }, - definition: getRecordOptimisticEffectDefinition({ - objectMetadataItem: foundObjectMetadataItem, - }), - }); - } - }, }); return { records, loading, fetchMoreRecords, + setRecordTableData, }; }; diff --git a/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx b/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx index 7a51a25b2..935b2ec06 100644 --- a/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx +++ b/front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx @@ -12,7 +12,6 @@ import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenu import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; import { RecordTableScopeInternalContext } from '@/ui/object/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector'; -import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; type useRecordTableContextMenuEntriesProps = { @@ -31,7 +30,6 @@ export const useRecordTableContextMenuEntries = ( const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); - const setTableRowIds = useSetRecoilState(tableRowIdsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector); const { scopeId: objectNamePlural, resetTableRowSelection } = useRecordTable({ @@ -76,16 +74,11 @@ export const useRecordTableContextMenuEntries = ( .getValue(); resetTableRowSelection(); - - if (deleteOneRecord) { - for (const rowId of rowIdsToDelete) { + await Promise.all( + rowIdsToDelete.map(async (rowId) => { await deleteOneRecord(rowId); - } - - setTableRowIds((tableRowIds) => - tableRowIds.filter((id) => !rowIdsToDelete.includes(id)), - ); - } + }), + ); }); return { diff --git a/front/src/modules/object-record/hooks/useSetRecordTableData.ts b/front/src/modules/object-record/hooks/useSetRecordTableData.ts deleted file mode 100644 index f954b180b..000000000 --- a/front/src/modules/object-record/hooks/useSetRecordTableData.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState'; -import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; -import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState'; -import { numberOfTableRowsState } from '@/ui/object/record-table/states/numberOfTableRowsState'; -import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; -import { useViewBar } from '@/views/hooks/useViewBar'; - -export const useSetRecordTableData = () => { - const { resetTableRowSelection } = useRecordTable(); - const { setEntityCountInCurrentView } = useViewBar(); - - return useRecoilCallback( - ({ set, snapshot }) => - >(newEntityArray: T[]) => { - for (const entity of newEntityArray) { - const currentEntity = snapshot - .getLoadable(entityFieldsFamilyState(entity.id)) - .valueOrThrow(); - - if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) { - set(entityFieldsFamilyState(entity.id), entity); - } - } - - const entityIds = newEntityArray.map((entity) => entity.id); - - set(tableRowIdsState, (currentRowIds) => { - if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) { - return entityIds; - } - - return currentRowIds; - }); - - resetTableRowSelection(); - - set(numberOfTableRowsState, entityIds.length); - setEntityCountInCurrentView(entityIds.length); - - set(isFetchingRecordTableDataState, false); - }, - [resetTableRowSelection, setEntityCountInCurrentView], - ); -}; diff --git a/front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 35613c44a..2966b1b2a 100644 --- a/front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -1,5 +1,4 @@ -import { useMutation } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; +import { useApolloClient } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; @@ -17,7 +16,7 @@ export const useUpdateOneRecord = ({ objectNameSingular, }); - const [mutateUpdateOneRecord] = useMutation(updateOneRecordMutation); + const apolloClient = useApolloClient(); const updateOneRecord = async ({ idToUpdate, @@ -30,7 +29,8 @@ export const useUpdateOneRecord = ({ }) => { const cachedRecord = getRecordFromCache(idToUpdate); - const updatedRecord = await mutateUpdateOneRecord({ + const updatedRecord = await apolloClient.mutate({ + mutation: updateOneRecordMutation, variables: { idToUpdate: idToUpdate, input: { @@ -43,12 +43,12 @@ export const useUpdateOneRecord = ({ ...input, }, }, - refetchQueries: forceRefetch - ? [getOperationName(findManyRecordsQuery) ?? ''] - : undefined, - awaitRefetchQueries: forceRefetch, }); + if (!updatedRecord?.data) { + return null; + } + return updatedRecord.data[ `update${capitalize(objectMetadataItem.nameSingular)}` ] as T; diff --git a/front/src/modules/object-record/types/PaginatedRecordTypeResults.ts b/front/src/modules/object-record/types/PaginatedRecordTypeResults.ts index f0fb2350e..ec2c83453 100644 --- a/front/src/modules/object-record/types/PaginatedRecordTypeResults.ts +++ b/front/src/modules/object-record/types/PaginatedRecordTypeResults.ts @@ -3,6 +3,7 @@ export type PaginatedRecordTypeEdge< > = { node: RecordType; cursor: string; + __typename?: string; }; export type PaginatedRecordTypeResults< diff --git a/front/src/modules/ui/object/record-board/components/RecordBoard.tsx b/front/src/modules/ui/object/record-board/components/RecordBoard.tsx index a24c47b1e..5db4ffe58 100644 --- a/front/src/modules/ui/object/record-board/components/RecordBoard.tsx +++ b/front/src/modules/ui/object/record-board/components/RecordBoard.tsx @@ -11,7 +11,6 @@ import { RecordBoardInternalEffect } from '@/ui/object/record-board/components/R import { RecordBoardContextMenu } from '@/ui/object/record-board/context-menu/components/RecordBoardContextMenu'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; import { useSetRecordBoardCardSelectedInternal } from '@/ui/object/record-board/hooks/internal/useSetRecordBoardCardSelectedInternal'; -import { useUpdateRecordBoardCardIdsInternal } from '@/ui/object/record-board/hooks/internal/useUpdateRecordBoardCardIdsInternal'; import { RecordBoardScope } from '@/ui/object/record-board/scopes/RecordBoardScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -94,21 +93,14 @@ export const RecordBoard = ({ callback: unselectAllActiveCards, }); - const updateBoardCardIds = useUpdateRecordBoardCardIdsInternal({ - recordBoardScopeId, - }); - const onDragEnd: OnDragEndResponder = useCallback( async (result) => { if (!boardColumns) return; - updateBoardCardIds(result); - try { const draggedEntityId = result.draggableId; const destinationColumnId = result.destination?.droppableId; - // TODO: abstract if ( draggedEntityId && destinationColumnId && @@ -123,7 +115,7 @@ export const RecordBoard = ({ logError(e); } }, - [boardColumns, updatePipelineProgressStageInDB, updateBoardCardIds], + [boardColumns, updatePipelineProgressStageInDB], ); const sortedBoardColumns = [...boardColumns].sort((a, b) => { diff --git a/front/src/modules/ui/object/record-board/components/RecordBoardColumn.tsx b/front/src/modules/ui/object/record-board/components/RecordBoardColumn.tsx index d8378dfb3..25671a4a1 100644 --- a/front/src/modules/ui/object/record-board/components/RecordBoardColumn.tsx +++ b/front/src/modules/ui/object/record-board/components/RecordBoardColumn.tsx @@ -1,24 +1,17 @@ -import React, { useState } from 'react'; +import React from 'react'; import styled from '@emotion/styled'; import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd'; import { useRecoilValue } from 'recoil'; -import { IconDotsVertical } from '@/ui/display/icon'; -import { Tag } from '@/ui/display/tag/components/Tag'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { RecordBoardCard } from '@/ui/object/record-board/components/RecordBoardCard'; +import { RecordBoardColumnHeader } from '@/ui/object/record-board/components/RecordBoardColumnHeader'; import { BoardCardIdContext } from '@/ui/object/record-board/contexts/BoardCardIdContext'; import { BoardColumnDefinition } from '@/ui/object/record-board/types/BoardColumnDefinition'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { BoardColumnContext } from '../contexts/BoardColumnContext'; import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState'; -import { recordBoardColumnTotalsFamilySelector } from '../states/selectors/recordBoardColumnTotalsFamilySelector'; -import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope'; import { BoardOptions } from '../types/BoardOptions'; -import { RecordBoardColumnDropdownMenu } from './RecordBoardColumnDropdownMenu'; - const StyledPlaceholder = styled.div` min-height: 1px; `; @@ -47,40 +40,6 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>` position: relative; `; -const StyledHeader = styled.div` - align-items: center; - cursor: pointer; - display: flex; - flex-direction: row; - height: 24px; - justify-content: left; - margin-bottom: ${({ theme }) => theme.spacing(2)}; - width: 100%; -`; - -const StyledAmount = styled.div` - color: ${({ theme }) => theme.font.color.tertiary}; - margin-left: ${({ theme }) => theme.spacing(2)}; -`; - -const StyledNumChildren = styled.div` - align-items: center; - background-color: ${({ theme }) => theme.background.tertiary}; - border-radius: ${({ theme }) => theme.border.radius.rounded}; - color: ${({ theme }) => theme.font.color.tertiary}; - display: flex; - height: 20px; - justify-content: center; - line-height: ${({ theme }) => theme.text.lineHeight.lg}; - margin-left: auto; - width: 16px; -`; - -const StyledHeaderActions = styled.div` - display: flex; - margin-left: auto; -`; - type BoardColumnCardsContainerProps = { children: React.ReactNode; droppableProvided: DroppableProvided; @@ -119,30 +78,6 @@ export const RecordBoardColumn = ({ onDelete, onTitleEdit, }: RecordBoardColumnProps) => { - const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); - const [isHeaderHovered, setIsHeaderHovered] = useState(false); - - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); - - const handleBoardColumnMenuOpen = () => { - setIsBoardColumnMenuOpen(true); - setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, { - goto: false, - }); - }; - - const handleBoardColumnMenuClose = () => { - goBackToPreviousHotkeyScope(); - setIsBoardColumnMenuOpen(false); - }; - - const boardColumnTotal = useRecoilValue( - recordBoardColumnTotalsFamilySelector(recordBoardColumnId), - ); - const cardIds = useRecoilValue( recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId), ); @@ -165,53 +100,12 @@ export const RecordBoardColumn = ({ {(droppableProvided) => ( - setIsHeaderHovered(true)} - onMouseLeave={() => setIsHeaderHovered(false)} - > - - {!!boardColumnTotal && ( - ${boardColumnTotal} - )} - {!isHeaderHovered && ( - {cardIds.length} - )} - {isHeaderHovered && ( - - - {/* {}} - /> */} - - )} - - {isBoardColumnMenuOpen && ( - - )} - - {isBoardColumnMenuOpen && ( - - )} + {cardIds.map((cardId, index) => ( diff --git a/front/src/modules/ui/object/record-board/components/RecordBoardColumnHeader.tsx b/front/src/modules/ui/object/record-board/components/RecordBoardColumnHeader.tsx new file mode 100644 index 000000000..2337a1400 --- /dev/null +++ b/front/src/modules/ui/object/record-board/components/RecordBoardColumnHeader.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { IconDotsVertical } from '@/ui/display/icon'; +import { Tag } from '@/ui/display/tag/components/Tag'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { recordBoardColumnTotalsFamilySelector } from '@/ui/object/record-board/states/selectors/recordBoardColumnTotalsFamilySelector'; +import { BoardColumnDefinition } from '@/ui/object/record-board/types/BoardColumnDefinition'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; + +import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState'; +import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope'; + +import { RecordBoardColumnDropdownMenu } from './RecordBoardColumnDropdownMenu'; + +const StyledHeader = styled.div` + align-items: center; + cursor: pointer; + display: flex; + flex-direction: row; + height: 24px; + justify-content: left; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledAmount = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledNumChildren = styled.div` + align-items: center; + background-color: ${({ theme }) => theme.background.tertiary}; + border-radius: ${({ theme }) => theme.border.radius.rounded}; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + height: 20px; + justify-content: center; + line-height: ${({ theme }) => theme.text.lineHeight.lg}; + margin-left: auto; + width: 16px; +`; + +const StyledHeaderActions = styled.div` + display: flex; + margin-left: auto; +`; + +type RecordBoardColumnHeaderProps = { + recordBoardColumnId: string; + columnDefinition: BoardColumnDefinition; + onDelete?: (columnId: string) => void; + onTitleEdit: (columnId: string, title: string, color: string) => void; +}; + +export const RecordBoardColumnHeader = ({ + recordBoardColumnId, + columnDefinition, + onDelete, + onTitleEdit, +}: RecordBoardColumnHeaderProps) => { + const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false); + const [isHeaderHovered, setIsHeaderHovered] = useState(false); + + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + + const handleBoardColumnMenuOpen = () => { + setIsBoardColumnMenuOpen(true); + setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, { + goto: false, + }); + }; + + const handleBoardColumnMenuClose = () => { + goBackToPreviousHotkeyScope(); + setIsBoardColumnMenuOpen(false); + }; + + const boardColumnTotal = useRecoilValue( + recordBoardColumnTotalsFamilySelector(recordBoardColumnId), + ); + + const cardIds = useRecoilValue( + recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId), + ); + + const handleTitleEdit = (title: string, color: string) => { + onTitleEdit(recordBoardColumnId, title, color); + }; + + return ( + <> + setIsHeaderHovered(true)} + onMouseLeave={() => setIsHeaderHovered(false)} + > + + {!!boardColumnTotal && ${boardColumnTotal}} + {!isHeaderHovered && ( + {cardIds.length} + )} + {isHeaderHovered && ( + + + {/* {}} + /> */} + + )} + + {isBoardColumnMenuOpen && ( + + )} + + ); +}; diff --git a/front/src/modules/ui/object/record-board/components/RecordBoardInternalEffect.tsx b/front/src/modules/ui/object/record-board/components/RecordBoardInternalEffect.tsx index 7cbaf6c98..3a2319e23 100644 --- a/front/src/modules/ui/object/record-board/components/RecordBoardInternalEffect.tsx +++ b/front/src/modules/ui/object/record-board/components/RecordBoardInternalEffect.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; -import { useObjectRecordBoard } from '@/object-record/hooks/useObjectRecordBoard'; +import { useObjectRecordBoard } from '@/object-record/hooks/useObjectRecordBoard.1'; import { useRecordBoardActionBarEntriesInternal } from '@/ui/object/record-board/hooks/internal/useRecordBoardActionBarEntriesInternal'; import { useRecordBoardContextMenuEntriesInternal } from '@/ui/object/record-board/hooks/internal/useRecordBoardContextMenuEntriesInternal'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; @@ -18,7 +18,24 @@ export const RecordBoardInternalEffect = ({}) => { const { setActionBarEntries } = useRecordBoardActionBarEntriesInternal(); const { setContextMenuEntries } = useRecordBoardContextMenuEntriesInternal(); - const { fetchMoreOpportunities, fetchMoreCompanies } = useObjectRecordBoard(); + const { + savedPipelineStepsState, + savedOpportunitiesState, + savedCompaniesState, + } = useRecordBoardScopedStates(); + + const { fetchMoreOpportunities, fetchMoreCompanies, opportunities } = + useObjectRecordBoard(); + + const [savedOpportunities, setSavedOpportunities] = useRecoilState( + savedOpportunitiesState, + ); + const savedPipelineSteps = useRecoilValue(savedPipelineStepsState); + const savedCompanies = useRecoilValue(savedCompaniesState); + + useEffect(() => { + setSavedOpportunities(opportunities); + }, [opportunities, setSavedOpportunities]); useEffect(() => { if (isDefined(fetchMoreOpportunities)) { @@ -32,16 +49,6 @@ export const RecordBoardInternalEffect = ({}) => { } }, [fetchMoreCompanies]); - const { - savedPipelineStepsState, - savedOpportunitiesState, - savedCompaniesState, - } = useRecordBoardScopedStates(); - - const savedPipelineSteps = useRecoilValue(savedPipelineStepsState); - const savedOpportunities = useRecoilValue(savedOpportunitiesState); - const savedCompanies = useRecoilValue(savedCompaniesState); - useEffect(() => { if (savedOpportunities && savedCompanies) { setActionBarEntries(); diff --git a/front/src/modules/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal.ts b/front/src/modules/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal.ts deleted file mode 100644 index bbe4bf101..000000000 --- a/front/src/modules/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { Opportunity } from '@/pipeline/types/Opportunity'; -import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; -import { recordBoardCardIdsByColumnIdFamilyState } from '@/ui/object/record-board/states/recordBoardCardIdsByColumnIdFamilyState'; - -export const useUpdateCompanyBoardCardIdsInternal = () => { - const { boardColumnsState } = useRecordBoardScopedStates(); - - return useRecoilCallback( - ({ snapshot, set }) => - (pipelineProgresses: Pick[]) => { - const boardColumns = snapshot - .getLoadable(boardColumnsState) - .valueOrThrow(); - - for (const boardColumn of boardColumns) { - const boardCardIds = pipelineProgresses - .filter((pipelineProgressToFilter) => { - return pipelineProgressToFilter.pipelineStepId === boardColumn.id; - }) - .map((pipelineProgress) => pipelineProgress.id); - - set( - recordBoardCardIdsByColumnIdFamilyState(boardColumn.id), - boardCardIds, - ); - } - }, - [boardColumnsState], - ); -}; diff --git a/front/src/modules/ui/object/record-board/hooks/internal/useUpdateRecordBoardCardIdsInternal.ts b/front/src/modules/ui/object/record-board/hooks/internal/useUpdateRecordBoardCardIdsInternal.ts deleted file mode 100644 index 6979cd403..000000000 --- a/front/src/modules/ui/object/record-board/hooks/internal/useUpdateRecordBoardCardIdsInternal.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { DropResult } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; -import { RecordBoardScopeInternalContext } from '@/ui/object/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; -import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; - -import { recordBoardCardIdsByColumnIdFamilyState } from '../../states/recordBoardCardIdsByColumnIdFamilyState'; -import { BoardColumnDefinition } from '../../types/BoardColumnDefinition'; - -type useUpdateRecordBoardCardIdsInternalProps = { - recordBoardScopeId?: string; -}; - -export const useUpdateRecordBoardCardIdsInternal = ( - props: useUpdateRecordBoardCardIdsInternalProps, -) => { - const scopeId = useAvailableScopeIdOrThrow( - RecordBoardScopeInternalContext, - props?.recordBoardScopeId, - ); - - const { boardColumnsState } = useRecordBoardScopedStates({ - recordBoardScopeId: scopeId, - }); - - return useRecoilCallback( - ({ snapshot, set }) => - (result: DropResult) => { - const currentBoardColumns = snapshot - .getLoadable(boardColumnsState) - .valueOrThrow(); - - const newBoardColumns = [...currentBoardColumns]; - - const { destination, source } = result; - - if (!destination) return; - - const sourceColumnIndex = newBoardColumns.findIndex( - (boardColumn: BoardColumnDefinition) => - boardColumn.id === source.droppableId, - ); - - const sourceColumn = newBoardColumns[sourceColumnIndex]; - - const destinationColumnIndex = newBoardColumns.findIndex( - (boardColumn: BoardColumnDefinition) => - boardColumn.id === destination.droppableId, - ); - - const destinationColumn = newBoardColumns[destinationColumnIndex]; - - if (!destinationColumn || !sourceColumn) return; - - const sourceCardIds = [ - ...snapshot - .getLoadable( - recordBoardCardIdsByColumnIdFamilyState(sourceColumn.id), - ) - .valueOrThrow(), - ]; - - const destinationCardIds = [ - ...snapshot - .getLoadable( - recordBoardCardIdsByColumnIdFamilyState(destinationColumn.id), - ) - .valueOrThrow(), - ]; - - const destinationIndex = - destination.index >= destinationCardIds.length - ? destinationCardIds.length - 1 - : destination.index; - - if (sourceColumn.id === destinationColumn.id) { - const [deletedCardId] = sourceCardIds.splice(source.index, 1); - - sourceCardIds.splice(destinationIndex, 0, deletedCardId); - - set( - recordBoardCardIdsByColumnIdFamilyState(sourceColumn.id), - sourceCardIds, - ); - } else { - const [removedCardId] = sourceCardIds.splice(source.index, 1); - - destinationCardIds.splice(destinationIndex, 0, removedCardId); - - set( - recordBoardCardIdsByColumnIdFamilyState(sourceColumn.id), - sourceCardIds, - ); - - set( - recordBoardCardIdsByColumnIdFamilyState(destinationColumn.id), - destinationCardIds, - ); - } - - return newBoardColumns; - }, - [boardColumnsState], - ); -}; diff --git a/front/src/modules/ui/object/record-table/components/RecordTableBody.tsx b/front/src/modules/ui/object/record-table/components/RecordTableBody.tsx index 9d20eb55a..f84c52155 100644 --- a/front/src/modules/ui/object/record-table/components/RecordTableBody.tsx +++ b/front/src/modules/ui/object/record-table/components/RecordTableBody.tsx @@ -1,9 +1,6 @@ -import { useEffect } from 'react'; import { useInView } from 'react-intersection-observer'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState'; import { RecordTableRow, @@ -11,33 +8,34 @@ import { } from '@/ui/object/record-table/components/RecordTableRow'; import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext'; import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext'; -import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates'; +import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; import { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState'; - -import { useRecordTable } from '../hooks/useRecordTable'; -import { tableRowIdsState } from '../states/tableRowIdsState'; +import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState'; +import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates'; export const RecordTableBody = () => { - const { ref: lastTableRowRef, inView: lastTableRowIsVisible } = useInView(); + const { scopeId } = useRecordTable(); + + const onLastRowVisible = useRecoilCallback( + ({ set }) => + async (inView: boolean) => { + const { tableLastRowVisibleState } = getRecordTableScopedStates({ + recordTableScopeId: scopeId, + }); + + set(tableLastRowVisibleState, inView); + }, + [scopeId], + ); + + const { ref: lastTableRowRef } = useInView({ + onChange: onLastRowVisible, + }); const tableRowIds = useRecoilValue(tableRowIdsState); - const { scopeId: objectNamePlural } = useRecordTable(); - const { tableLastRowVisibleState } = useRecordTableScopedStates(); - const setTableLastRowVisible = useSetRecoilState(tableLastRowVisibleState); - - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural, - }); - - const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem( - { - objectNameSingular, - }, - ); - const [isFetchingMoreObjects] = useRecoilState( - isFetchingMoreRecordsFamilyState(foundObjectMetadataItem?.namePlural), + isFetchingMoreRecordsFamilyState(scopeId), ); const isFetchingRecordTableData = useRecoilValue( @@ -45,10 +43,6 @@ export const RecordTableBody = () => { ); const lastRowId = tableRowIds[tableRowIds.length - 1]; - useEffect(() => { - setTableLastRowVisible(lastTableRowIsVisible); - }, [lastTableRowIsVisible, setTableLastRowVisible]); - if (isFetchingRecordTableData) { return <>; } @@ -60,7 +54,11 @@ export const RecordTableBody = () => { 30 + ? lastTableRowRef + : undefined + } rowId={rowId} /> diff --git a/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx b/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx index 8a9d090ef..00701a6d2 100644 --- a/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx +++ b/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx @@ -6,10 +6,18 @@ import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/inter import { isDefined } from '~/utils/isDefined'; export const RecordTableBodyEffect = () => { - const { fetchMoreRecords: fetchMoreObjects } = useObjectRecordTable(); + const { + fetchMoreRecords: fetchMoreObjects, + records, + setRecordTableData, + } = useObjectRecordTable(); const { tableLastRowVisibleState } = useRecordTableScopedStates(); const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState); + useEffect(() => { + setRecordTableData(records); + }, [records, setRecordTableData]); + useEffect(() => { if (tableLastRowVisible && isDefined(fetchMoreObjects)) { fetchMoreObjects(); diff --git a/front/src/modules/ui/object/record-table/hooks/internal/useSetRecordTableData.ts b/front/src/modules/ui/object/record-table/hooks/internal/useSetRecordTableData.ts index c0bcbcbaa..7c8a4cbb8 100644 --- a/front/src/modules/ui/object/record-table/hooks/internal/useSetRecordTableData.ts +++ b/front/src/modules/ui/object/record-table/hooks/internal/useSetRecordTableData.ts @@ -1,6 +1,7 @@ import { useRecoilCallback } from 'recoil'; import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isFetchingRecordTableDataState } from '../../states/isFetchingRecordTableDataState'; import { numberOfTableRowsState } from '../../states/numberOfTableRowsState'; @@ -29,16 +30,13 @@ export const useSetRecordTableData = ({ set(entityFieldsFamilyState(entity.id), entity); } } + const currentRowIds = snapshot.getLoadable(tableRowIdsState).getValue(); const entityIds = newEntityArray.map((entity) => entity.id); - set(tableRowIdsState, (currentRowIds) => { - if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) { - return entityIds; - } - - return currentRowIds; - }); + if (!isDeeplyEqual(currentRowIds, entityIds)) { + set(tableRowIdsState, entityIds); + } resetTableRowSelection(); diff --git a/front/src/modules/ui/object/record-table/hooks/useUpsertTableRowId.ts b/front/src/modules/ui/object/record-table/hooks/useUpsertTableRowId.ts deleted file mode 100644 index 540aea6a9..000000000 --- a/front/src/modules/ui/object/record-table/hooks/useUpsertTableRowId.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { tableRowIdsState } from '../states/tableRowIdsState'; - -// Used only in company table and people table -// Remove after refactoring - -export const useUpsertTableRowId = () => - useRecoilCallback( - ({ set, snapshot }) => - (rowId: string) => { - const currentRowIds = snapshot - .getLoadable(tableRowIdsState) - .valueOrThrow(); - - set(tableRowIdsState, Array.from(new Set([rowId, ...currentRowIds]))); - }, - [], - );