mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-27 22:32:49 +03:00
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:
parent
976e058328
commit
69f48ea330
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -11,6 +11,7 @@ type OptimisticEffectWriter<T> = ({
|
||||
cache: ApolloCache<T>;
|
||||
query: DocumentNode;
|
||||
newData: T;
|
||||
deletedRecordIds?: string[];
|
||||
variables: OperationVariables;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
isUsingFlexibleBackend?: boolean;
|
||||
|
@ -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: {
|
||||
|
@ -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`
|
||||
|
@ -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`
|
||||
|
@ -16,15 +16,19 @@ 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 (newData) {
|
||||
if (!draft) {
|
||||
return {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
edges: [{ node: newData, cursor: '' }],
|
||||
pageInfo: {
|
||||
endCursor: '',
|
||||
@ -35,7 +39,26 @@ export const getRecordOptimisticEffectDefinition = ({
|
||||
};
|
||||
}
|
||||
|
||||
draft.edges.unshift({ node: newData, cursor: '' });
|
||||
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`,
|
||||
});
|
||||
}
|
||||
|
||||
if (deletedRecordIds) {
|
||||
draft.edges = draft.edges.filter(
|
||||
(edge) => !deletedRecordIds.includes(edge.node.id),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return newRecordPaginatedCacheField;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -214,7 +214,7 @@ export const useFindManyRecords = <
|
||||
|
||||
return {
|
||||
objectMetadataItem,
|
||||
records,
|
||||
records: records as RecordType[],
|
||||
loading,
|
||||
error,
|
||||
fetchMoreRecords,
|
||||
|
177
front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts
Normal file
177
front/src/modules/object-record/hooks/useGenerateEmptyRecord.ts
Normal 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,
|
||||
};
|
||||
};
|
104
front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts
Normal file
104
front/src/modules/object-record/hooks/useObjectRecordBoard.1.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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);
|
||||
onCompleted: useCallback(() => {
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
[setIsBoardLoaded, setSavedOpportunities, updateCompanyBoardCardIds],
|
||||
),
|
||||
}, [setIsBoardLoaded]),
|
||||
});
|
||||
|
||||
const { fetchMoreRecords: fetchMoreCompanies } = useFindManyRecords({
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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],
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -3,6 +3,7 @@ export type PaginatedRecordTypeEdge<
|
||||
> = {
|
||||
node: RecordType;
|
||||
cursor: string;
|
||||
__typename?: string;
|
||||
};
|
||||
|
||||
export type PaginatedRecordTypeResults<
|
||||
|
@ -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) => {
|
||||
|
@ -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}
|
||||
<RecordBoardColumnHeader
|
||||
recordBoardColumnId={recordBoardColumnId}
|
||||
columnDefinition={columnDefinition}
|
||||
onDelete={onDelete}
|
||||
onTitleEdit={handleTitleEdit}
|
||||
stageId={recordBoardColumnId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBoardColumnMenuOpen && (
|
||||
<RecordBoardColumnDropdownMenu
|
||||
onClose={handleBoardColumnMenuClose}
|
||||
onDelete={onDelete}
|
||||
onTitleEdit={handleTitleEdit}
|
||||
stageId={recordBoardColumnId}
|
||||
/>
|
||||
)}
|
||||
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
|
||||
{cardIds.map((cardId, index) => (
|
||||
<BoardCardIdContext.Provider value={cardId} key={cardId}>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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],
|
||||
);
|
||||
};
|
@ -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],
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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,17 +30,14 @@ 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;
|
||||
if (!isDeeplyEqual(currentRowIds, entityIds)) {
|
||||
set(tableRowIdsState, entityIds);
|
||||
}
|
||||
|
||||
return currentRowIds;
|
||||
});
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
set(numberOfTableRowsState, entityIds.length);
|
||||
|
@ -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])));
|
||||
},
|
||||
[],
|
||||
);
|
Loading…
Reference in New Issue
Block a user