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
This commit is contained in:
Charles Bochet 2023-12-05 22:29:27 +01:00 committed by GitHub
parent 976e058328
commit 69f48ea330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 606 additions and 465 deletions

View File

@ -49,12 +49,14 @@ export const useOptimisticEffect = ({
const optimisticEffectWriter = ({
cache,
newData,
deletedRecordIds,
query,
variables,
objectMetadataItem,
}: {
cache: ApolloCache<unknown>;
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,

View File

@ -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;

View File

@ -11,6 +11,7 @@ type OptimisticEffectWriter<T> = ({
cache: ApolloCache<T>;
query: DocumentNode;
newData: T;
deletedRecordIds?: string[];
variables: OperationVariables;
objectMetadataItem?: ObjectMetadataItem;
isUsingFlexibleBackend?: boolean;

View File

@ -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<Person>({
const { records: companies } = useFindManyRecords<Company>({
skip: !isCommandMenuOpened,
objectNameSingular: 'company',
filter: {
@ -139,7 +141,7 @@ export const CommandMenu = () => {
limit: 3,
});
const { records: activities } = useFindManyRecords<Person>({
const { records: activities } = useFindManyRecords<Activity>({
skip: !isCommandMenuOpened,
objectNameSingular: 'activity',
filter: {

View File

@ -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`

View File

@ -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`

View File

@ -16,26 +16,49 @@ export const getRecordOptimisticEffectDefinition = ({
resolver: ({
currentData,
newData,
deletedRecordIds,
}: {
currentData: unknown;
newData: unknown;
newData: { id: string } & Record<string, any>;
deletedRecordIds?: string[];
}) => {
const newRecordPaginatedCacheField = produce<
PaginatedRecordTypeResults<any>
>(currentData as PaginatedRecordTypeResults<any>, (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;

View File

@ -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 = <T>({
@ -20,21 +21,42 @@ export const useCreateOneRecord = <T>({
);
// TODO: type this with a minimal type at least with Record<string, any>
const [mutate] = useMutation(createOneRecordMutation);
const apolloClient = useApolloClient();
const { generateEmptyRecord } = useGenerateEmptyRecord({
objectMetadataItem,
});
const createOneRecord = async (input: Record<string, any>) => {
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;

View File

@ -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 = <T>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
{
@ -17,16 +21,15 @@ export const useDeleteOneRecord = <T>({
},
);
// TODO: type this with a minimal type at least with Record<string, any>
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 = <T>({
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 {

View File

@ -214,7 +214,7 @@ export const useFindManyRecords = <
return {
objectMetadataItem,
records,
records: records as RecordType[],
loading,
error,
fetchMoreRecords,

View File

@ -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,
};
};

View File

@ -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<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
},
[setSavedPipelineSteps],
),
});
const {
records: opportunities,
loading,
fetchMoreRecords: fetchMoreOpportunities,
} = useFindManyRecords<Opportunity>({
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<Company>) => {
setSavedCompanies(data.edges.map((edge) => edge.node));
},
[setSavedCompanies],
),
});
return {
opportunities,
loading,
fetchMoreOpportunities,
fetchMoreCompanies,
};
};

View File

@ -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<Opportunity>({
skip: !savedPipelineSteps.length,
objectNameSingular: 'opportunity',
filter: filter,
orderBy: orderBy,
onCompleted: useCallback(
(data: PaginatedRecordTypeResults<Opportunity>) => {
const pipelineProgresses: Array<Opportunity> = data.edges.map(
(edge) => edge.node,
);
updateCompanyBoardCardIds(pipelineProgresses);
setSavedOpportunities(pipelineProgresses);
setIsBoardLoaded(true);
},
[setIsBoardLoaded, setSavedOpportunities, updateCompanyBoardCardIds],
),
onCompleted: useCallback(() => {
setIsBoardLoaded(true);
}, [setIsBoardLoaded]),
});
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({

View File

@ -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,
};
};

View File

@ -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 {

View File

@ -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 }) =>
<T extends { id: string } & Record<string, any>>(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],
);
};

View File

@ -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 = <T>({
objectNameSingular,
});
const [mutateUpdateOneRecord] = useMutation(updateOneRecordMutation);
const apolloClient = useApolloClient();
const updateOneRecord = async ({
idToUpdate,
@ -30,7 +29,8 @@ export const useUpdateOneRecord = <T>({
}) => {
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 = <T>({
...input,
},
},
refetchQueries: forceRefetch
? [getOperationName(findManyRecordsQuery) ?? '']
: undefined,
awaitRefetchQueries: forceRefetch,
});
if (!updatedRecord?.data) {
return null;
}
return updatedRecord.data[
`update${capitalize(objectMetadataItem.nameSingular)}`
] as T;

View File

@ -3,6 +3,7 @@ export type PaginatedRecordTypeEdge<
> = {
node: RecordType;
cursor: string;
__typename?: string;
};
export type PaginatedRecordTypeResults<

View File

@ -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) => {

View File

@ -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 = ({
<Droppable droppableId={recordBoardColumnId}>
{(droppableProvided) => (
<StyledColumn isFirstColumn={isFirstColumn}>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
>
<Tag
onClick={handleBoardColumnMenuOpen}
color={columnDefinition.colorCode ?? 'gray'}
text={columnDefinition.title}
/>
{!!boardColumnTotal && (
<StyledAmount>${boardColumnTotal}</StyledAmount>
)}
{!isHeaderHovered && (
<StyledNumChildren>{cardIds.length}</StyledNumChildren>
)}
{isHeaderHovered && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
{/* <LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => {}}
/> */}
</StyledHeaderActions>
)}
</StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
onDelete={onDelete}
onTitleEdit={handleTitleEdit}
stageId={recordBoardColumnId}
/>
)}
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
onDelete={onDelete}
onTitleEdit={handleTitleEdit}
stageId={recordBoardColumnId}
/>
)}
<RecordBoardColumnHeader
recordBoardColumnId={recordBoardColumnId}
columnDefinition={columnDefinition}
onDelete={onDelete}
onTitleEdit={handleTitleEdit}
/>
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
{cardIds.map((cardId, index) => (
<BoardCardIdContext.Provider value={cardId} key={cardId}>

View File

@ -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 (
<>
<StyledHeader
onMouseEnter={() => setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
>
<Tag
onClick={handleBoardColumnMenuOpen}
color={columnDefinition.colorCode ?? 'gray'}
text={columnDefinition.title}
/>
{!!boardColumnTotal && <StyledAmount>${boardColumnTotal}</StyledAmount>}
{!isHeaderHovered && (
<StyledNumChildren>{cardIds.length}</StyledNumChildren>
)}
{isHeaderHovered && (
<StyledHeaderActions>
<LightIconButton
accent="tertiary"
Icon={IconDotsVertical}
onClick={handleBoardColumnMenuOpen}
/>
{/* <LightIconButton
accent="tertiary"
Icon={IconPlus}
onClick={() => {}}
/> */}
</StyledHeaderActions>
)}
</StyledHeader>
{isBoardColumnMenuOpen && (
<RecordBoardColumnDropdownMenu
onClose={handleBoardColumnMenuClose}
onDelete={onDelete}
onTitleEdit={handleTitleEdit}
stageId={recordBoardColumnId}
/>
)}
</>
);
};

View File

@ -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();

View File

@ -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<Opportunity, 'pipelineStepId' | 'id'>[]) => {
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],
);
};

View File

@ -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],
);
};

View File

@ -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 = () => {
<RowIndexContext.Provider value={rowIndex}>
<RecordTableRow
key={rowId}
ref={rowId === lastRowId ? lastTableRowRef : undefined}
ref={
rowId === lastRowId && rowIndex > 30
? lastTableRowRef
: undefined
}
rowId={rowId}
/>
</RowIndexContext.Provider>

View File

@ -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();

View File

@ -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();

View File

@ -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])));
},
[],
);