From a46210bb806479f926b0cea3916ac74766a7505b Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 4 Sep 2023 10:56:48 +0200 Subject: [PATCH] Implement Optimistic Effects (#1415) * Fix person deletion not reflected on Opportunities POC * Fix companies, user deletion * Implement optimistic effects * Implement optimistic effects * Implement optimistic effects * Fix accoding to PR --- .../hooks/useOptimisticEffect.ts | 48 +++++++++++++++++++ .../states/optimisticEffectState.ts | 10 ++++ .../types/OptimisticEffect.ts | 18 +++++++ .../getCompaniesOptimisticEffect.ts | 47 ++++++++++++++++++ .../table/components/CompanyTable.tsx | 2 + .../getPeopleOptimisticEffect.ts | 45 +++++++++++++++++ .../people/table/components/PeopleTable.tsx | 2 + .../components/GenericEntityTableData.tsx | 9 ++++ front/src/pages/companies/Companies.tsx | 3 ++ front/src/pages/people/People.tsx | 5 +- 10 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts create mode 100644 front/src/modules/apollo/optimistic-effect/states/optimisticEffectState.ts create mode 100644 front/src/modules/apollo/optimistic-effect/types/OptimisticEffect.ts create mode 100644 front/src/modules/companies/graphql/optimistic-effects/getCompaniesOptimisticEffect.ts create mode 100644 front/src/modules/people/graphql/optimistic-effect-callback/getPeopleOptimisticEffect.ts diff --git a/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts new file mode 100644 index 000000000..17e584a07 --- /dev/null +++ b/front/src/modules/apollo/optimistic-effect/hooks/useOptimisticEffect.ts @@ -0,0 +1,48 @@ +import { useApolloClient } from '@apollo/client'; +import { useRecoilCallback } from 'recoil'; + +import { optimisticEffectState } from '../states/optimisticEffectState'; +import { OptimisticEffect } from '../types/OptimisticEffect'; + +export function useOptimisticEffect() { + const apolloClient = useApolloClient(); + + const registerOptimisticEffect = useRecoilCallback( + ({ snapshot, set }) => + (optimisticEffect: OptimisticEffect) => { + const { key } = optimisticEffect; + const optimisticEffects = snapshot + .getLoadable(optimisticEffectState) + .getValue(); + + set(optimisticEffectState, { + ...optimisticEffects, + [key]: optimisticEffect, + }); + }, + ); + + const triggerOptimisticEffects = useRecoilCallback( + ({ snapshot }) => + (typename: string, entities: any[]) => { + const optimisticEffects = snapshot + .getLoadable(optimisticEffectState) + .getValue(); + + Object.values(optimisticEffects).forEach((optimisticEffect) => { + if (optimisticEffect.typename === typename) { + optimisticEffect.resolver({ + cache: apolloClient.cache, + entities, + variables: optimisticEffect.variables, + }); + } + }); + }, + ); + + return { + registerOptimisticEffect, + triggerOptimisticEffects, + }; +} diff --git a/front/src/modules/apollo/optimistic-effect/states/optimisticEffectState.ts b/front/src/modules/apollo/optimistic-effect/states/optimisticEffectState.ts new file mode 100644 index 000000000..37219471e --- /dev/null +++ b/front/src/modules/apollo/optimistic-effect/states/optimisticEffectState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +import { OptimisticEffect } from '../types/OptimisticEffect'; + +export const optimisticEffectState = atom< + Record> +>({ + key: 'optimisticEffectState', + default: {}, +}); diff --git a/front/src/modules/apollo/optimistic-effect/types/OptimisticEffect.ts b/front/src/modules/apollo/optimistic-effect/types/OptimisticEffect.ts new file mode 100644 index 000000000..ff8431599 --- /dev/null +++ b/front/src/modules/apollo/optimistic-effect/types/OptimisticEffect.ts @@ -0,0 +1,18 @@ +import { ApolloCache } from '@apollo/client'; + +type OptimisticEffectResolver = ({ + cache, + entities, + variables, +}: { + cache: ApolloCache; + entities: T[]; + variables: QueryVariables; +}) => void; + +export type OptimisticEffect = { + key: string; + typename: string; + variables: QueryVariables; + resolver: OptimisticEffectResolver; +}; diff --git a/front/src/modules/companies/graphql/optimistic-effects/getCompaniesOptimisticEffect.ts b/front/src/modules/companies/graphql/optimistic-effects/getCompaniesOptimisticEffect.ts new file mode 100644 index 000000000..1327d288b --- /dev/null +++ b/front/src/modules/companies/graphql/optimistic-effects/getCompaniesOptimisticEffect.ts @@ -0,0 +1,47 @@ +import { ApolloCache } from '@apollo/client'; + +import { + Company, + GetCompaniesQuery, + GetCompaniesQueryVariables, +} from '~/generated/graphql'; + +import { GET_COMPANIES } from '../queries/getCompanies'; + +function optimisticEffectResolver({ + cache, + entities, + variables, +}: { + cache: ApolloCache; + entities: Company[]; + variables: GetCompaniesQueryVariables; +}) { + const existingData = cache.readQuery({ + query: GET_COMPANIES, + variables: { orderBy: variables.orderBy, where: variables.where }, + }); + + if (!existingData) { + return; + } + + cache.writeQuery({ + query: GET_COMPANIES, + variables: { orderBy: variables.orderBy, where: variables.where }, + data: { + companies: [...entities, ...existingData.companies], + }, + }); +} + +export function getCompaniesOptimisticEffect( + variables: GetCompaniesQueryVariables, +) { + return { + key: 'generic-entity-table-data-companies', + variables: variables, + typename: 'Company', + resolver: optimisticEffectResolver, + }; +} diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index dc32d428f..7730416d6 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -1,4 +1,5 @@ import { companiesAvailableColumnDefinitions } from '@/companies/constants/companiesAvailableColumnDefinitions'; +import { getCompaniesOptimisticEffect } from '@/companies/graphql/optimistic-effects/getCompaniesOptimisticEffect'; import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries'; import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries'; import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport'; @@ -53,6 +54,7 @@ export function CompanyTable() { ; + entities: Person[]; + variables: GetPeopleQueryVariables; +}) { + const existingData = cache.readQuery({ + query: GET_PEOPLE, + variables: { orderBy: variables.orderBy, where: variables.where }, + }); + + if (!existingData) { + return; + } + + cache.writeQuery({ + query: GET_PEOPLE, + variables: { orderBy: variables.orderBy, where: variables.where }, + data: { + people: [...entities, ...existingData.people], + }, + }); +} + +export function getPeopleOptimisticEffect(variables: GetPeopleQueryVariables) { + return { + key: 'generic-entity-table-data-person', + variables: variables, + typename: 'Person', + resolver: optimisticEffectResolver, + }; +} diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index 97b798945..fb08410c8 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -1,4 +1,5 @@ import { peopleAvailableColumnDefinitions } from '@/people/constants/peopleAvailableColumnDefinitions'; +import { getPeopleOptimisticEffect } from '@/people/graphql/optimistic-effect-callback/getPeopleOptimisticEffect'; import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries'; import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries'; import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport'; @@ -52,6 +53,7 @@ export function PeopleTable() { OptimisticEffect; orderBy?: any; whereFilters?: any; filterDefinitionArray: FilterDefinition[]; @@ -26,11 +30,16 @@ export function GenericEntityTableData({ setContextMenuEntries?: () => void; }) { const setEntityTableData = useSetEntityTableData(); + const { registerOptimisticEffect } = useOptimisticEffect(); + useGetRequest({ variables: { orderBy, where: whereFilters }, onCompleted: (data: any) => { const entities = data[getRequestResultKey] ?? []; setEntityTableData(entities, filterDefinitionArray); + registerOptimisticEffect( + getRequestOptimisticEffect({ orderBy, where: whereFilters }), + ); }, }); diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index 9114bf015..96f031824 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -3,6 +3,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { v4 } from 'uuid'; +import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { CompanyTable } from '@/companies/table/components/CompanyTable'; import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; @@ -30,6 +31,7 @@ export function Companies() { const [insertCompany] = useInsertOneCompanyMutation(); const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertTableRowIds = useUpsertTableRowId(); + const { triggerOptimisticEffects } = useOptimisticEffect(); async function handleAddButtonClick() { const newCompanyId: string = v4(); @@ -61,6 +63,7 @@ export function Companies() { if (data?.createOneCompany) { upsertTableRowIds(data?.createOneCompany.id); upsertEntityTableItem(data?.createOneCompany); + triggerOptimisticEffects('Company', [data?.createOneCompany]); } }, refetchQueries: [getOperationName(SEARCH_COMPANY_QUERY) ?? ''], diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 971d7b08c..f186ab7c2 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -2,6 +2,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { v4 } from 'uuid'; +import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { PeopleTable } from '@/people/table/components/PeopleTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext'; @@ -28,6 +29,7 @@ export function People() { const [insertOnePerson] = useInsertOnePersonMutation(); const upsertEntityTableItem = useUpsertEntityTableItem(); const upsertTableRowIds = useUpsertTableRowId(); + const { triggerOptimisticEffects } = useOptimisticEffect(); async function handleAddButtonClick() { const newPersonId: string = v4(); @@ -50,10 +52,11 @@ export function People() { createdAt: '', }, }, - update: (cache, { data }) => { + update: (_cache, { data }) => { if (data?.createOnePerson) { upsertTableRowIds(data?.createOnePerson.id); upsertEntityTableItem(data?.createOnePerson); + triggerOptimisticEffects('Person', [data?.createOnePerson]); } }, });