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
28 changed files with 606 additions and 465 deletions

View File

@ -49,12 +49,14 @@ export const useOptimisticEffect = ({
const optimisticEffectWriter = ({ const optimisticEffectWriter = ({
cache, cache,
newData, newData,
deletedRecordIds,
query, query,
variables, variables,
objectMetadataItem, objectMetadataItem,
}: { }: {
cache: ApolloCache<unknown>; cache: ApolloCache<unknown>;
newData: unknown; newData: unknown;
deletedRecordIds?: string[];
variables: OperationVariables; variables: OperationVariables;
query: DocumentNode; query: DocumentNode;
isUsingFlexibleBackend?: boolean; isUsingFlexibleBackend?: boolean;
@ -79,6 +81,7 @@ export const useOptimisticEffect = ({
objectMetadataItem.namePlural objectMetadataItem.namePlural
], ],
newData, newData,
deletedRecordIds,
variables, variables,
}), }),
}, },
@ -116,7 +119,7 @@ export const useOptimisticEffect = ({
const triggerOptimisticEffects = useRecoilCallback( const triggerOptimisticEffects = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
(typename: string, newData: unknown) => { (typename: string, newData: unknown, deletedRecordIds?: string[]) => {
const optimisticEffects = snapshot const optimisticEffects = snapshot
.getLoadable(optimisticEffectState) .getLoadable(optimisticEffectState)
.getValue(); .getValue();
@ -135,6 +138,7 @@ export const useOptimisticEffect = ({
cache: apolloClient.cache, cache: apolloClient.cache,
query: optimisticEffect.query ?? ({} as DocumentNode), query: optimisticEffect.query ?? ({} as DocumentNode),
newData: formattedNewData, newData: formattedNewData,
deletedRecordIds,
variables: optimisticEffect.variables, variables: optimisticEffect.variables,
isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend, isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend,
objectMetadataItem: optimisticEffect.objectMetadataItem, objectMetadataItem: optimisticEffect.objectMetadataItem,

View File

@ -3,9 +3,11 @@ import { OperationVariables } from '@apollo/client';
export type OptimisticEffectResolver = ({ export type OptimisticEffectResolver = ({
currentData, currentData,
newData, newData,
deletedRecordIds,
variables, variables,
}: { }: {
currentData: any; //TODO: Change when decommissioning v1 currentData: any; //TODO: Change when decommissioning v1
newData: any; //TODO: Change when decommissioning v1 newData: any; //TODO: Change when decommissioning v1
deletedRecordIds?: string[];
variables: OperationVariables; variables: OperationVariables;
}) => void; }) => void;

View File

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

View File

@ -3,7 +3,9 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity';
import { CommandMenuSelectableListEffect } from '@/command-menu/components/CommandMenuSelectableListEffect'; import { CommandMenuSelectableListEffect } from '@/command-menu/components/CommandMenuSelectableListEffect';
import { Company } from '@/companies/types/Company';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { Person } from '@/people/types/Person'; import { Person } from '@/people/types/Person';
@ -130,7 +132,7 @@ export const CommandMenu = () => {
limit: 3, limit: 3,
}); });
const { records: companies } = useFindManyRecords<Person>({ const { records: companies } = useFindManyRecords<Company>({
skip: !isCommandMenuOpened, skip: !isCommandMenuOpened,
objectNameSingular: 'company', objectNameSingular: 'company',
filter: { filter: {
@ -139,7 +141,7 @@ export const CommandMenu = () => {
limit: 3, limit: 3,
}); });
const { records: activities } = useFindManyRecords<Person>({ const { records: activities } = useFindManyRecords<Activity>({
skip: !isCommandMenuOpened, skip: !isCommandMenuOpened,
objectNameSingular: 'activity', objectNameSingular: 'activity',
filter: { filter: {

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordTable } from '@/ui/object/record-table/components/RecordTable'; import { RecordTable } from '@/ui/object/record-table/components/RecordTable';
import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId'; import { TableOptionsDropdownId } from '@/ui/object/record-table/constants/TableOptionsDropdownId';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; 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 { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useUpdateOneRecord } from '../hooks/useUpdateOneRecord';
import { RecordTableEffect } from './RecordTableEffect'; import { RecordTableEffect } from './RecordTableEffect';
const StyledContainer = styled.div` const StyledContainer = styled.div`

View File

@ -6,6 +6,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { IconBuildingSkyscraper } from '@/ui/display/icon'; import { IconBuildingSkyscraper } from '@/ui/display/icon';
import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody'; 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 { RecordTableActionBar } from '@/ui/object/record-table/action-bar/components/RecordTableActionBar';
import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu'; import { RecordTableContextMenu } from '@/ui/object/record-table/context-menu/components/RecordTableContextMenu';
import { useCreateOneRecord } from '../hooks/useCreateOneRecord';
import { RecordTableContainer } from './RecordTableContainer'; import { RecordTableContainer } from './RecordTableContainer';
const StyledTableContainer = styled.div` const StyledTableContainer = styled.div`

View File

@ -16,26 +16,49 @@ export const getRecordOptimisticEffectDefinition = ({
resolver: ({ resolver: ({
currentData, currentData,
newData, newData,
deletedRecordIds,
}: { }: {
currentData: unknown; currentData: unknown;
newData: unknown; newData: { id: string } & Record<string, any>;
deletedRecordIds?: string[];
}) => { }) => {
const newRecordPaginatedCacheField = produce< const newRecordPaginatedCacheField = produce<
PaginatedRecordTypeResults<any> PaginatedRecordTypeResults<any>
>(currentData as PaginatedRecordTypeResults<any>, (draft) => { >(currentData as PaginatedRecordTypeResults<any>, (draft) => {
if (!draft) { if (newData) {
return { if (!draft) {
edges: [{ node: newData, cursor: '' }], return {
pageInfo: { __typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
endCursor: '', edges: [{ node: newData, cursor: '' }],
hasNextPage: false, pageInfo: {
hasPreviousPage: false, endCursor: '',
startCursor: '', 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; return newRecordPaginatedCacheField;

View File

@ -1,9 +1,10 @@
import { useMutation } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
export const useCreateOneRecord = <T>({ 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> // 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 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: { variables: {
input: { ...input, id: v4() }, input: { ...input, id: recordId },
},
optimisticResponse: {
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
generateEmptyRecord(recordId),
}, },
}); });
if (!createdObject.data) {
return null;
}
triggerOptimisticEffects( triggerOptimisticEffects(
`${capitalize(objectMetadataItem.nameSingular)}Edge`, `${capitalize(objectMetadataItem.nameSingular)}Edge`,
createdObject.data[ createdObject.data[
`create${capitalize(objectMetadataItem.nameSingular)}` `create${capitalize(objectMetadataItem.nameSingular)}`
], ],
); );
return createdObject.data[ return createdObject.data[
`create${capitalize(objectMetadataItem.nameSingular)}` `create${capitalize(objectMetadataItem.nameSingular)}`
] as T; ] as T;

View File

@ -1,6 +1,7 @@
import { useCallback } from 'react'; 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 { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
@ -10,6 +11,9 @@ export const useDeleteOneRecord = <T>({
objectNameSingular, objectNameSingular,
}: ObjectMetadataItemIdentifier) => { }: ObjectMetadataItemIdentifier) => {
const { performOptimisticEvict } = useOptimisticEvict(); const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem( 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 apolloClient = useApolloClient();
const [mutate] = useMutation(deleteOneRecordMutation);
const deleteOneRecord = useCallback( const deleteOneRecord = useCallback(
async (idToDelete: string) => { async (idToDelete: string) => {
const deletedRecord = await mutate({ triggerOptimisticEffects(
variables: { `${capitalize(objectMetadataItem.nameSingular)}Edge`,
idToDelete, undefined,
}, [idToDelete],
}); );
performOptimisticEvict( performOptimisticEvict(
capitalize(objectMetadataItem.nameSingular), capitalize(objectMetadataItem.nameSingular),
@ -34,11 +37,24 @@ export const useDeleteOneRecord = <T>({
idToDelete, idToDelete,
); );
const deletedRecord = await apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete,
},
});
return deletedRecord.data[ return deletedRecord.data[
`create${capitalize(objectMetadataItem.nameSingular)}` `create${capitalize(objectMetadataItem.nameSingular)}`
] as T; ] as T;
}, },
[performOptimisticEvict, objectMetadataItem, mutate], [
triggerOptimisticEffects,
objectMetadataItem.nameSingular,
performOptimisticEvict,
apolloClient,
deleteOneRecordMutation,
],
); );
return { return {

View File

@ -214,7 +214,7 @@ export const useFindManyRecords = <
return { return {
objectMetadataItem, objectMetadataItem,
records, records: records as RecordType[],
loading, loading,
error, error,
fetchMoreRecords, 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 { turnFiltersIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause';
import { turnSortsIntoOrderBy } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/ui/object/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates';
import { useUpdateCompanyBoardCardIdsInternal } from '@/ui/object/record-board/hooks/internal/useUpdateCompanyBoardCardIdsInternal';
import { useFindManyRecords } from './useFindManyRecords'; import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordBoard = () => { export const useObjectRecordBoard = () => {
const objectNameSingular = 'opportunity'; const objectNameSingular = 'opportunity';
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIdsInternal();
const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem( const { objectMetadataItem: foundObjectMetadataItem } = useObjectMetadataItem(
{ {
@ -71,24 +69,14 @@ export const useObjectRecordBoard = () => {
records: opportunities, records: opportunities,
loading, loading,
fetchMoreRecords: fetchMoreOpportunities, fetchMoreRecords: fetchMoreOpportunities,
} = useFindManyRecords({ } = useFindManyRecords<Opportunity>({
skip: !savedPipelineSteps.length, skip: !savedPipelineSteps.length,
objectNameSingular: 'opportunity', objectNameSingular: 'opportunity',
filter: filter, filter: filter,
orderBy: orderBy, orderBy: orderBy,
onCompleted: useCallback( onCompleted: useCallback(() => {
(data: PaginatedRecordTypeResults<Opportunity>) => { setIsBoardLoaded(true);
const pipelineProgresses: Array<Opportunity> = data.edges.map( }, [setIsBoardLoaded]),
(edge) => edge.node,
);
updateCompanyBoardCardIds(pipelineProgresses);
setSavedOpportunities(pipelineProgresses);
setIsBoardLoaded(true);
},
[setIsBoardLoaded, setSavedOpportunities, updateCompanyBoardCardIds],
),
}); });
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({ const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({

View File

@ -1,6 +1,5 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { turnFiltersIntoWhereClause } from '@/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause'; 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 { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/internal/useRecordTableScopedStates';
import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { getRecordOptimisticEffectDefinition } from '../graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
import { useFindManyRecords } from './useFindManyRecords'; import { useFindManyRecords } from './useFindManyRecords';
export const useObjectRecordTable = () => { export const useObjectRecordTable = () => {
@ -25,10 +22,6 @@ export const useObjectRecordTable = () => {
}, },
); );
const { registerOptimisticEffect } = useOptimisticEffect({
objectNameSingular,
});
const { tableFiltersState, tableSortsState } = useRecordTableScopedStates(); const { tableFiltersState, tableSortsState } = useRecordTableScopedStates();
const tableFilters = useRecoilValue(tableFiltersState); const tableFilters = useRecoilValue(tableFiltersState);
@ -47,25 +40,12 @@ export const useObjectRecordTable = () => {
objectNameSingular, objectNameSingular,
filter, filter,
orderBy, 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 { return {
records, records,
loading, loading,
fetchMoreRecords, 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 { useRecordTable } from '@/ui/object/record-table/hooks/useRecordTable';
import { RecordTableScopeInternalContext } from '@/ui/object/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; import { RecordTableScopeInternalContext } from '@/ui/object/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector'; import { selectedRowIdsSelector } from '@/ui/object/record-table/states/selectors/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRecordTableContextMenuEntriesProps = { type useRecordTableContextMenuEntriesProps = {
@ -31,7 +30,6 @@ export const useRecordTableContextMenuEntries = (
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
const setTableRowIds = useSetRecoilState(tableRowIdsState);
const selectedRowIds = useRecoilValue(selectedRowIdsSelector); const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
const { scopeId: objectNamePlural, resetTableRowSelection } = useRecordTable({ const { scopeId: objectNamePlural, resetTableRowSelection } = useRecordTable({
@ -76,16 +74,11 @@ export const useRecordTableContextMenuEntries = (
.getValue(); .getValue();
resetTableRowSelection(); resetTableRowSelection();
await Promise.all(
if (deleteOneRecord) { rowIdsToDelete.map(async (rowId) => {
for (const rowId of rowIdsToDelete) {
await deleteOneRecord(rowId); await deleteOneRecord(rowId);
} }),
);
setTableRowIds((tableRowIds) =>
tableRowIds.filter((id) => !rowIdsToDelete.includes(id)),
);
}
}); });
return { 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 { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
@ -17,7 +16,7 @@ export const useUpdateOneRecord = <T>({
objectNameSingular, objectNameSingular,
}); });
const [mutateUpdateOneRecord] = useMutation(updateOneRecordMutation); const apolloClient = useApolloClient();
const updateOneRecord = async ({ const updateOneRecord = async ({
idToUpdate, idToUpdate,
@ -30,7 +29,8 @@ export const useUpdateOneRecord = <T>({
}) => { }) => {
const cachedRecord = getRecordFromCache(idToUpdate); const cachedRecord = getRecordFromCache(idToUpdate);
const updatedRecord = await mutateUpdateOneRecord({ const updatedRecord = await apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: { variables: {
idToUpdate: idToUpdate, idToUpdate: idToUpdate,
input: { input: {
@ -43,12 +43,12 @@ export const useUpdateOneRecord = <T>({
...input, ...input,
}, },
}, },
refetchQueries: forceRefetch
? [getOperationName(findManyRecordsQuery) ?? '']
: undefined,
awaitRefetchQueries: forceRefetch,
}); });
if (!updatedRecord?.data) {
return null;
}
return updatedRecord.data[ return updatedRecord.data[
`update${capitalize(objectMetadataItem.nameSingular)}` `update${capitalize(objectMetadataItem.nameSingular)}`
] as T; ] as T;

View File

@ -3,6 +3,7 @@ export type PaginatedRecordTypeEdge<
> = { > = {
node: RecordType; node: RecordType;
cursor: string; cursor: string;
__typename?: string;
}; };
export type PaginatedRecordTypeResults< 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 { RecordBoardContextMenu } from '@/ui/object/record-board/context-menu/components/RecordBoardContextMenu';
import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates';
import { useSetRecordBoardCardSelectedInternal } from '@/ui/object/record-board/hooks/internal/useSetRecordBoardCardSelectedInternal'; 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 { RecordBoardScope } from '@/ui/object/record-board/scopes/RecordBoardScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -94,21 +93,14 @@ export const RecordBoard = ({
callback: unselectAllActiveCards, callback: unselectAllActiveCards,
}); });
const updateBoardCardIds = useUpdateRecordBoardCardIdsInternal({
recordBoardScopeId,
});
const onDragEnd: OnDragEndResponder = useCallback( const onDragEnd: OnDragEndResponder = useCallback(
async (result) => { async (result) => {
if (!boardColumns) return; if (!boardColumns) return;
updateBoardCardIds(result);
try { try {
const draggedEntityId = result.draggableId; const draggedEntityId = result.draggableId;
const destinationColumnId = result.destination?.droppableId; const destinationColumnId = result.destination?.droppableId;
// TODO: abstract
if ( if (
draggedEntityId && draggedEntityId &&
destinationColumnId && destinationColumnId &&
@ -123,7 +115,7 @@ export const RecordBoard = ({
logError(e); logError(e);
} }
}, },
[boardColumns, updatePipelineProgressStageInDB, updateBoardCardIds], [boardColumns, updatePipelineProgressStageInDB],
); );
const sortedBoardColumns = [...boardColumns].sort((a, b) => { 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 styled from '@emotion/styled';
import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd'; import { Draggable, Droppable, DroppableProvided } from '@hello-pangea/dnd';
import { useRecoilValue } from 'recoil'; 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 { 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 { BoardCardIdContext } from '@/ui/object/record-board/contexts/BoardCardIdContext';
import { BoardColumnDefinition } from '@/ui/object/record-board/types/BoardColumnDefinition'; import { BoardColumnDefinition } from '@/ui/object/record-board/types/BoardColumnDefinition';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { BoardColumnContext } from '../contexts/BoardColumnContext'; import { BoardColumnContext } from '../contexts/BoardColumnContext';
import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState'; import { recordBoardCardIdsByColumnIdFamilyState } from '../states/recordBoardCardIdsByColumnIdFamilyState';
import { recordBoardColumnTotalsFamilySelector } from '../states/selectors/recordBoardColumnTotalsFamilySelector';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardOptions } from '../types/BoardOptions'; import { BoardOptions } from '../types/BoardOptions';
import { RecordBoardColumnDropdownMenu } from './RecordBoardColumnDropdownMenu';
const StyledPlaceholder = styled.div` const StyledPlaceholder = styled.div`
min-height: 1px; min-height: 1px;
`; `;
@ -47,40 +40,6 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
position: relative; 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 = { type BoardColumnCardsContainerProps = {
children: React.ReactNode; children: React.ReactNode;
droppableProvided: DroppableProvided; droppableProvided: DroppableProvided;
@ -119,30 +78,6 @@ export const RecordBoardColumn = ({
onDelete, onDelete,
onTitleEdit, onTitleEdit,
}: RecordBoardColumnProps) => { }: 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( const cardIds = useRecoilValue(
recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId), recordBoardCardIdsByColumnIdFamilyState(recordBoardColumnId),
); );
@ -165,53 +100,12 @@ export const RecordBoardColumn = ({
<Droppable droppableId={recordBoardColumnId}> <Droppable droppableId={recordBoardColumnId}>
{(droppableProvided) => ( {(droppableProvided) => (
<StyledColumn isFirstColumn={isFirstColumn}> <StyledColumn isFirstColumn={isFirstColumn}>
<StyledHeader <RecordBoardColumnHeader
onMouseEnter={() => setIsHeaderHovered(true)} recordBoardColumnId={recordBoardColumnId}
onMouseLeave={() => setIsHeaderHovered(false)} columnDefinition={columnDefinition}
> onDelete={onDelete}
<Tag onTitleEdit={handleTitleEdit}
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}
/>
)}
<BoardColumnCardsContainer droppableProvided={droppableProvided}> <BoardColumnCardsContainer droppableProvided={droppableProvided}>
{cardIds.map((cardId, index) => ( {cardIds.map((cardId, index) => (
<BoardCardIdContext.Provider value={cardId} key={cardId}> <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 { 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 { useRecordBoardActionBarEntriesInternal } from '@/ui/object/record-board/hooks/internal/useRecordBoardActionBarEntriesInternal';
import { useRecordBoardContextMenuEntriesInternal } from '@/ui/object/record-board/hooks/internal/useRecordBoardContextMenuEntriesInternal'; import { useRecordBoardContextMenuEntriesInternal } from '@/ui/object/record-board/hooks/internal/useRecordBoardContextMenuEntriesInternal';
import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates'; import { useRecordBoardScopedStates } from '@/ui/object/record-board/hooks/internal/useRecordBoardScopedStates';
@ -18,7 +18,24 @@ export const RecordBoardInternalEffect = ({}) => {
const { setActionBarEntries } = useRecordBoardActionBarEntriesInternal(); const { setActionBarEntries } = useRecordBoardActionBarEntriesInternal();
const { setContextMenuEntries } = useRecordBoardContextMenuEntriesInternal(); 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(() => { useEffect(() => {
if (isDefined(fetchMoreOpportunities)) { if (isDefined(fetchMoreOpportunities)) {
@ -32,16 +49,6 @@ export const RecordBoardInternalEffect = ({}) => {
} }
}, [fetchMoreCompanies]); }, [fetchMoreCompanies]);
const {
savedPipelineStepsState,
savedOpportunitiesState,
savedCompaniesState,
} = useRecordBoardScopedStates();
const savedPipelineSteps = useRecoilValue(savedPipelineStepsState);
const savedOpportunities = useRecoilValue(savedOpportunitiesState);
const savedCompanies = useRecoilValue(savedCompaniesState);
useEffect(() => { useEffect(() => {
if (savedOpportunities && savedCompanies) { if (savedOpportunities && savedCompanies) {
setActionBarEntries(); 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 { 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 { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { import {
RecordTableRow, RecordTableRow,
@ -11,33 +8,34 @@ import {
} from '@/ui/object/record-table/components/RecordTableRow'; } from '@/ui/object/record-table/components/RecordTableRow';
import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext'; import { RowIdContext } from '@/ui/object/record-table/contexts/RowIdContext';
import { RowIndexContext } from '@/ui/object/record-table/contexts/RowIndexContext'; 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 { isFetchingRecordTableDataState } from '@/ui/object/record-table/states/isFetchingRecordTableDataState';
import { tableRowIdsState } from '@/ui/object/record-table/states/tableRowIdsState';
import { useRecordTable } from '../hooks/useRecordTable'; import { getRecordTableScopedStates } from '@/ui/object/record-table/utils/getRecordTableScopedStates';
import { tableRowIdsState } from '../states/tableRowIdsState';
export const RecordTableBody = () => { 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 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( const [isFetchingMoreObjects] = useRecoilState(
isFetchingMoreRecordsFamilyState(foundObjectMetadataItem?.namePlural), isFetchingMoreRecordsFamilyState(scopeId),
); );
const isFetchingRecordTableData = useRecoilValue( const isFetchingRecordTableData = useRecoilValue(
@ -45,10 +43,6 @@ export const RecordTableBody = () => {
); );
const lastRowId = tableRowIds[tableRowIds.length - 1]; const lastRowId = tableRowIds[tableRowIds.length - 1];
useEffect(() => {
setTableLastRowVisible(lastTableRowIsVisible);
}, [lastTableRowIsVisible, setTableLastRowVisible]);
if (isFetchingRecordTableData) { if (isFetchingRecordTableData) {
return <></>; return <></>;
} }
@ -60,7 +54,11 @@ export const RecordTableBody = () => {
<RowIndexContext.Provider value={rowIndex}> <RowIndexContext.Provider value={rowIndex}>
<RecordTableRow <RecordTableRow
key={rowId} key={rowId}
ref={rowId === lastRowId ? lastTableRowRef : undefined} ref={
rowId === lastRowId && rowIndex > 30
? lastTableRowRef
: undefined
}
rowId={rowId} rowId={rowId}
/> />
</RowIndexContext.Provider> </RowIndexContext.Provider>

View File

@ -6,10 +6,18 @@ import { useRecordTableScopedStates } from '@/ui/object/record-table/hooks/inter
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const RecordTableBodyEffect = () => { export const RecordTableBodyEffect = () => {
const { fetchMoreRecords: fetchMoreObjects } = useObjectRecordTable(); const {
fetchMoreRecords: fetchMoreObjects,
records,
setRecordTableData,
} = useObjectRecordTable();
const { tableLastRowVisibleState } = useRecordTableScopedStates(); const { tableLastRowVisibleState } = useRecordTableScopedStates();
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState); const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
useEffect(() => {
setRecordTableData(records);
}, [records, setRecordTableData]);
useEffect(() => { useEffect(() => {
if (tableLastRowVisible && isDefined(fetchMoreObjects)) { if (tableLastRowVisible && isDefined(fetchMoreObjects)) {
fetchMoreObjects(); fetchMoreObjects();

View File

@ -1,6 +1,7 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState'; import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isFetchingRecordTableDataState } from '../../states/isFetchingRecordTableDataState'; import { isFetchingRecordTableDataState } from '../../states/isFetchingRecordTableDataState';
import { numberOfTableRowsState } from '../../states/numberOfTableRowsState'; import { numberOfTableRowsState } from '../../states/numberOfTableRowsState';
@ -29,16 +30,13 @@ export const useSetRecordTableData = ({
set(entityFieldsFamilyState(entity.id), entity); set(entityFieldsFamilyState(entity.id), entity);
} }
} }
const currentRowIds = snapshot.getLoadable(tableRowIdsState).getValue();
const entityIds = newEntityArray.map((entity) => entity.id); const entityIds = newEntityArray.map((entity) => entity.id);
set(tableRowIdsState, (currentRowIds) => { if (!isDeeplyEqual(currentRowIds, entityIds)) {
if (JSON.stringify(currentRowIds) !== JSON.stringify(entityIds)) { set(tableRowIdsState, entityIds);
return entityIds; }
}
return currentRowIds;
});
resetTableRowSelection(); 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])));
},
[],
);