RecordPicker refactoring part 3: remove effects (#10505)

This PR is part 3 of RecordPicker refactoring. 

It aims to remove all effects from:
- (low level layer) SingleRecordPicker, MultipleRecordPicker
- (higher level layer) RelationPicker, RelationToOneInput,
RelationFromManyInput, ActivityTargetInput...

In this PR, I'm re-grouping Effects in ActivityTarget section and
creating a hook that will be called on inlineCellOpen
This commit is contained in:
Charles Bochet
2025-02-27 09:56:03 +01:00
committed by GitHub
parent aa25a80171
commit d246010c5c
28 changed files with 207 additions and 149 deletions

View File

@ -3,7 +3,10 @@ import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect';
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
import { MultipleObjectRecordOnClickOutsideEffect } from '@/activities/inline-cell/components/MultipleObjectRecordOnClickOutsideEffect';
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { Note } from '@/activities/types/Note';
@ -24,12 +27,10 @@ import {
objectRecordMultiSelectComponentFamilyState,
} from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/record-picker-morph-legacy/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/record-picker-morph-legacy/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect';
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { prefillRecord } from '@/object-record/utils/prefillRecord';
import { useRef } from 'react';
type ActivityTargetInlineCellEditModeProps = {
activity: Task | Note;
@ -257,20 +258,31 @@ export const ActivityTargetInlineCellEditMode = ({
],
);
const containerRef = useRef<HTMLDivElement>(null);
return (
<>
<RecordPickerComponentInstanceContext.Provider
value={{ instanceId: recordPickerInstanceId }}
>
<ActivityTargetObjectRecordEffect
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
<ActivityTargetObjectRecordEffect
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
/>
<ActivityTargetInlineCellEditModeMultiRecordsEffect
recordPickerInstanceId={recordPickerInstanceId}
selectedObjectRecordIds={selectedTargetObjectIds}
/>
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect
recordPickerInstanceId={recordPickerInstanceId}
/>
<MultipleObjectRecordOnClickOutsideEffect
containerRef={containerRef}
onClickOutside={closeEditableField}
/>
<div ref={containerRef}>
<MultipleRecordPicker
onSubmit={handleSubmit}
onChange={handleChange}
componentInstanceId={recordPickerInstanceId}
/>
<ActivityTargetInlineCellEditModeMultiRecordsEffect
selectedObjectRecordIds={selectedTargetObjectIds}
/>
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
<MultipleRecordPicker onSubmit={handleSubmit} onChange={handleChange} />
</RecordPickerComponentInstanceContext.Provider>
</div>
</>
);
};

View File

@ -0,0 +1,111 @@
import { useEffect } from 'react';
import {
useRecoilCallback,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState';
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
import { SelectedObjectRecordId } from '@/object-record/types/SelectedObjectRecordId';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
// Todo: this effect should be deprecated to use sync hooks
export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
recordPickerInstanceId,
selectedObjectRecordIds,
}: {
recordPickerInstanceId: string;
selectedObjectRecordIds: SelectedObjectRecordId[];
}) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
recordPickerInstanceId,
);
const {
objectRecordsIdsMultiSelectState,
objectRecordMultiSelectCheckedRecordsIdsState,
} = useObjectRecordMultiSelectScopedStates(instanceId);
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
useRecoilState(objectRecordsIdsMultiSelectState);
const setObjectRecordMultiSelectCheckedRecordsIds = useSetRecoilState(
objectRecordMultiSelectCheckedRecordsIdsState,
);
const updateRecords = useRecoilCallback(
({ snapshot, set }) =>
(newRecords: ObjectRecordForSelect[]) => {
for (const newRecord of newRecords) {
const currentRecord = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: instanceId,
familyKey: newRecord.record.id,
}),
)
.getValue();
const objectRecordMultiSelectCheckedRecordsIds = snapshot
.getLoadable(objectRecordMultiSelectCheckedRecordsIdsState)
.getValue();
const newRecordWithSelected = {
...newRecord,
selected: objectRecordMultiSelectCheckedRecordsIds.some(
(checkedRecordId) => checkedRecordId === newRecord.record.id,
),
};
if (
!isDeeplyEqual(
newRecordWithSelected.selected,
currentRecord?.selected,
)
) {
set(
objectRecordMultiSelectComponentFamilyState({
scopeId: instanceId,
familyKey: newRecordWithSelected.record.id,
}),
newRecordWithSelected,
);
}
}
},
[objectRecordMultiSelectCheckedRecordsIdsState, instanceId],
);
const matchesSearchFilterObjectRecords = useRecoilValue(
objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({
scopeId: instanceId,
}),
);
useEffect(() => {
const allRecords = matchesSearchFilterObjectRecords ?? [];
updateRecords(allRecords);
const allRecordsIds = allRecords.map((record) => record.record.id);
if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) {
setObjectRecordsIdsMultiSelect(allRecordsIds);
}
}, [
matchesSearchFilterObjectRecords,
objectRecordsIdsMultiSelect,
setObjectRecordsIdsMultiSelect,
updateRecords,
]);
useEffect(() => {
setObjectRecordMultiSelectCheckedRecordsIds(
selectedObjectRecordIds.map((rec) => rec.id),
);
}, [selectedObjectRecordIds, setObjectRecordMultiSelectCheckedRecordsIds]);
return <></>;
};

View File

@ -0,0 +1,51 @@
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { useMultiObjectSearch } from '@/activities/inline-cell/hooks/useMultiObjectSearch';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState';
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
// Todo: this effect should be deprecated to use sync hooks
export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect = ({
recordPickerInstanceId,
}: {
recordPickerInstanceId: string;
}) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordPickerComponentInstanceContext,
recordPickerInstanceId,
);
const setRecordMultiSelectMatchesFilterRecords = useSetRecoilState(
objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({
scopeId: instanceId,
}),
);
const recordPickerSearchFilter = useRecoilComponentValueV2(
recordPickerSearchFilterComponentState,
instanceId,
);
const { matchesSearchFilterObjectRecordsQueryResult } = useMultiObjectSearch({
excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note],
searchFilterValue: recordPickerSearchFilter,
limit: 10,
});
const { objectRecordForSelectArray } =
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
multiObjectRecordsQueryResult:
matchesSearchFilterObjectRecordsQueryResult,
});
useEffect(() => {
setRecordMultiSelectMatchesFilterRecords(objectRecordForSelectArray);
}, [setRecordMultiSelectMatchesFilterRecords, objectRecordForSelectArray]);
return <></>;
};

View File

@ -5,6 +5,7 @@ import { IconArrowUpRight, IconPencil } from 'twenty-ui';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
@ -59,6 +60,9 @@ export const ActivityTargetsInlineCell = ({
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
});
const { openActivityTargetInlineCellEditMode } =
useOpenActivityTargetInlineCellEditMode();
return (
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
<FieldFocusContextProvider>
@ -90,6 +94,11 @@ export const ActivityTargetsInlineCell = ({
maxWidth={maxWidth}
/>
),
onOpenEditMode: () => {
openActivityTargetInlineCellEditMode({
recordPickerInstanceId: `record-picker-${activity.id}`,
});
},
}}
>
<RecordInlineCellContainer />

View File

@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-picker/constants/RecordPickerClickOutsideListenerId';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
// Todo: this effect should be deprecated to use sync hooks
export const MultipleObjectRecordOnClickOutsideEffect = ({
containerRef,
onClickOutside,
}: {
containerRef: React.RefObject<HTMLDivElement>;
onClickOutside: () => void;
}) => {
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
useEffect(() => {
toggleRightDrawerClickOustideListener(false);
return () => {
toggleRightDrawerClickOustideListener(true);
};
}, [toggleRightDrawerClickOustideListener]);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onClickOutside();
},
listenerId: RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID,
});
return <></>;
};

View File

@ -0,0 +1,95 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RecordPickerComponentInstanceContext.Provider>
);
const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6';
const personId = 'ab091fd9-1b81-4dfd-bfdb-564ffee032a2';
describe('useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray', () => {
it('should return object formatted from objectMetadataItemsState', async () => {
const { result } = renderHook(
() => {
return {
formattedRecord:
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray(
{
multiObjectRecordsQueryResult: {
opportunities: {
edges: [
{
node: {
id: opportunityId,
pointOfContactId:
'e992bda7-d797-4e12-af04-9b427f42244c',
updatedAt: '2023-11-30T11:13:15.308Z',
createdAt: '2023-11-30T11:13:15.308Z',
__typename: 'Opportunity',
},
cursor: 'cursor',
__typename: 'OpportunityEdge',
},
],
pageInfo: {},
},
people: {
edges: [
{
node: {
id: personId,
updatedAt: '2023-11-30T11:13:15.308Z',
createdAt: '2023-11-30T11:13:15.308Z',
__typename: 'Person',
},
cursor: 'cursor',
__typename: 'PersonEdge',
},
],
pageInfo: {},
},
},
},
),
setObjectMetadata: useSetRecoilState(objectMetadataItemsState),
};
},
{
wrapper: Wrapper,
},
);
act(() => {
result.current.setObjectMetadata(generatedMockObjectMetadataItems);
});
expect(
result.current.formattedRecord.objectRecordForSelectArray.length,
).toBe(2);
const [opportunityRecordForSelect, personRecordForSelect] =
result.current.formattedRecord.objectRecordForSelectArray;
expect(opportunityRecordForSelect.objectMetadataItem.namePlural).toBe(
'opportunities',
);
expect(opportunityRecordForSelect.record.id).toBe(opportunityId);
expect(opportunityRecordForSelect.recordIdentifier.linkToShowPage).toBe(
`/object/opportunity/${opportunityId}`,
);
expect(personRecordForSelect.objectMetadataItem.namePlural).toBe('people');
expect(personRecordForSelect.record.id).toBe(personId);
expect(personRecordForSelect.recordIdentifier.linkToShowPage).toBe(
`/object/person/${personId}`,
);
});
});

View File

@ -0,0 +1,97 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/activities/inline-cell/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const instanceId = 'instanceId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
<RecoilRoot>{children}</RecoilRoot>
</RecordPickerComponentInstanceContext.Provider>
);
const opportunityId = 'cb702502-4b1d-488e-9461-df3fb096ebf6';
const personId = 'ab091fd9-1b81-4dfd-bfdb-564ffee032a2';
describe('useMultiObjectRecordsQueryResultFormattedAsObjectRecordsMap', () => {
it('should return object formatted from objectMetadataItemsState', async () => {
const { result } = renderHook(
() => {
return {
formattedRecord:
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
multiObjectRecordsQueryResult: {
opportunities: {
edges: [
{
node: {
id: opportunityId,
pointOfContactId:
'e992bda7-d797-4e12-af04-9b427f42244c',
updatedAt: '2023-11-30T11:13:15.308Z',
createdAt: '2023-11-30T11:13:15.308Z',
__typename: 'Opportunity',
},
cursor: 'cursor',
__typename: 'OpportunityEdge',
},
],
pageInfo: {},
},
people: {
edges: [
{
node: {
id: personId,
updatedAt: '2023-11-30T11:13:15.308Z',
createdAt: '2023-11-30T11:13:15.308Z',
__typename: 'Person',
},
cursor: 'cursor',
__typename: 'PersonEdge',
},
],
pageInfo: {},
},
},
}),
setObjectMetadata: useSetRecoilState(objectMetadataItemsState),
};
},
{
wrapper: Wrapper,
},
);
act(() => {
result.current.setObjectMetadata(generatedMockObjectMetadataItems);
});
expect(
Object.values(result.current.formattedRecord.objectRecordsMap).flat()
.length,
).toBe(2);
const opportunityObjectRecords =
result.current.formattedRecord.objectRecordsMap.opportunities;
const personObjectRecords =
result.current.formattedRecord.objectRecordsMap.people;
expect(opportunityObjectRecords[0].objectMetadataItem.namePlural).toBe(
'opportunities',
);
expect(opportunityObjectRecords[0].record.id).toBe(opportunityId);
expect(opportunityObjectRecords[0].recordIdentifier.linkToShowPage).toBe(
`/object/opportunity/${opportunityId}`,
);
expect(personObjectRecords[0].objectMetadataItem.namePlural).toBe('people');
expect(personObjectRecords[0].record.id).toBe(personId);
expect(personObjectRecords[0].recordIdentifier.linkToShowPage).toBe(
`/object/person/${personId}`,
);
});
});

View File

@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsByNamePluralMapSelector } from '@/object-metadata/states/objectMetadataItemsByNamePluralMapSelector';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
import { formatMultiObjectRecordSearchResults } from '@/object-record/multiple-objects/utils/formatMultiObjectRecordSearchResults';
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
import { isDefined } from 'twenty-shared';
export const useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray =
({
multiObjectRecordsQueryResult,
}: {
multiObjectRecordsQueryResult:
| MultiObjectRecordQueryResult
| null
| undefined;
}) => {
const objectMetadataItemsByNamePluralMap = useRecoilValue(
objectMetadataItemsByNamePluralMapSelector,
);
const formattedMultiObjectRecordsQueryResult = useMemo(() => {
return formatMultiObjectRecordSearchResults(
multiObjectRecordsQueryResult,
);
}, [multiObjectRecordsQueryResult]);
const objectRecordForSelectArray = useMemo(() => {
return Object.entries(
formattedMultiObjectRecordsQueryResult ?? {},
).flatMap(([namePlural, objectRecordConnection]) => {
const objectMetadataItem =
objectMetadataItemsByNamePluralMap.get(namePlural);
if (!isDefined(objectMetadataItem)) return [];
return objectRecordConnection.edges.map(({ node }) => ({
objectMetadataItem,
record: node,
recordIdentifier: getObjectRecordIdentifier({
objectMetadataItem,
record: node,
}),
})) as ObjectRecordForSelect[];
});
}, [
formattedMultiObjectRecordsQueryResult,
objectMetadataItemsByNamePluralMap,
]);
return {
objectRecordForSelectArray,
};
};

View File

@ -0,0 +1,66 @@
import { useQuery } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery';
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
import { isObjectMetadataItemSearchableInCombinedRequest } from '@/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest';
import { isDefined } from 'twenty-shared';
export const useMultiObjectSearch = ({
searchFilterValue,
limit,
excludedObjects,
}: {
searchFilterValue: string;
limit?: number;
excludedObjects?: CoreObjectNameSingular[];
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const selectableObjectMetadataItems = objectMetadataItems
.filter(({ isSystem, isRemote }) => !isSystem && !isRemote)
.filter(({ nameSingular }) => {
return !excludedObjects?.includes(nameSingular as CoreObjectNameSingular);
})
.filter((objectMetadataItem) =>
isObjectMetadataItemSearchableInCombinedRequest(objectMetadataItem),
);
const { limitPerMetadataItem } = useLimitPerMetadataItem({
objectMetadataItems,
limit,
});
const multiSelectSearchQueryForSelectedIds =
useGenerateCombinedSearchRecordsQuery({
operationSignatures: selectableObjectMetadataItems.map(
(objectMetadataItem) => ({
objectNameSingular: objectMetadataItem.nameSingular,
variables: {},
}),
),
});
const {
loading: matchesSearchFilterObjectRecordsLoading,
data: matchesSearchFilterObjectRecordsQueryResult,
} = useQuery<MultiObjectRecordQueryResult>(
multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY,
{
variables: {
search: searchFilterValue,
...limitPerMetadataItem,
},
skip: !isDefined(multiSelectSearchQueryForSelectedIds),
},
);
return {
matchesSearchFilterObjectRecordsLoading,
matchesSearchFilterObjectRecordsQueryResult,
};
};

View File

@ -0,0 +1,61 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsByNamePluralMapSelector } from '@/object-metadata/states/objectMetadataItemsByNamePluralMapSelector';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
import { formatMultiObjectRecordSearchResults } from '@/object-record/multiple-objects/utils/formatMultiObjectRecordSearchResults';
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
import { isDefined } from 'twenty-shared';
export const useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap = ({
multiObjectRecordsQueryResult,
}: {
multiObjectRecordsQueryResult:
| MultiObjectRecordQueryResult
| null
| undefined;
}) => {
const objectMetadataItemsByNamePluralMap = useRecoilValue(
objectMetadataItemsByNamePluralMapSelector,
);
const formattedMultiObjectRecordsQueryResult = useMemo(() => {
return formatMultiObjectRecordSearchResults(multiObjectRecordsQueryResult);
}, [multiObjectRecordsQueryResult]);
const objectRecordsMap = useMemo(() => {
const recordsByNamePlural: { [key: string]: ObjectRecordForSelect[] } = {};
Object.entries(formattedMultiObjectRecordsQueryResult ?? {}).forEach(
([namePlural, objectRecordConnection]) => {
const objectMetadataItem =
objectMetadataItemsByNamePluralMap.get(namePlural);
if (!isDefined(objectMetadataItem)) return [];
if (!isDefined(recordsByNamePlural[namePlural])) {
recordsByNamePlural[namePlural] = [];
}
objectRecordConnection.edges.forEach(({ node }) => {
const record = {
objectMetadataItem,
record: node,
recordIdentifier: getObjectRecordIdentifier({
objectMetadataItem,
record: node,
}),
} as ObjectRecordForSelect;
recordsByNamePlural[namePlural].push(record);
});
},
);
return recordsByNamePlural;
}, [
formattedMultiObjectRecordsQueryResult,
objectMetadataItemsByNamePluralMap,
]);
return {
objectRecordsMap,
};
};

View File

@ -0,0 +1,100 @@
import { gql, useQuery } from '@apollo/client';
import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
import { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem';
import { useOrderByFieldPerMetadataItem } from '@/object-metadata/hooks/useOrderByFieldPerMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
import { SelectedObjectRecordId } from '@/object-record/types/SelectedObjectRecordId';
import { capitalize, isDefined } from 'twenty-shared';
export const EMPTY_QUERY = gql`
query Empty {
__typename
}
`;
export const useMultiObjectSearchSelectedItemsQuery = ({
selectedObjectRecordIds,
}: {
selectedObjectRecordIds: SelectedObjectRecordId[];
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter(
({ nameSingular }) => {
return selectedObjectRecordIds.some(({ objectNameSingular }) => {
return objectNameSingular === nameSingular;
});
},
);
const selectedIdFilterPerMetadataItem = Object.fromEntries(
objectMetadataItemsUsedInSelectedIdsQuery
.map(({ nameSingular }) => {
const selectedIds = selectedObjectRecordIds
.filter(
({ objectNameSingular }) => objectNameSingular === nameSingular,
)
.map(({ id }) => id);
if (!isNonEmptyArray(selectedIds)) return null;
return [
`filter${capitalize(nameSingular)}`,
{
id: {
in: selectedIds,
},
},
];
})
.filter(isDefined),
);
const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
});
const { limitPerMetadataItem } = useLimitPerMetadataItem({
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
});
const multiSelectQueryForSelectedIds =
useGenerateCombinedFindManyRecordsQuery({
operationSignatures: objectMetadataItemsUsedInSelectedIdsQuery.map(
(objectMetadataItem) => ({
objectNameSingular: objectMetadataItem.nameSingular,
variables: {},
}),
),
});
const {
loading: selectedObjectRecordsLoading,
data: selectedObjectRecordsQueryResult,
} = useQuery<MultiObjectRecordQueryResult>(
multiSelectQueryForSelectedIds ?? EMPTY_QUERY,
{
variables: {
...selectedIdFilterPerMetadataItem,
...orderByFieldPerMetadataItem,
...limitPerMetadataItem,
},
skip: !isDefined(multiSelectQueryForSelectedIds),
},
);
const { objectRecordForSelectArray: selectedObjectRecords } =
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
multiObjectRecordsQueryResult: selectedObjectRecordsQueryResult,
});
return {
selectedObjectRecordsLoading,
selectedObjectRecords,
};
};

View File

@ -0,0 +1,14 @@
type OpenActivityTargetInlineCellEditModeProps = {
recordPickerInstanceId: string;
};
export const useOpenActivityTargetInlineCellEditMode = () => {
const openActivityTargetInlineCellEditMode = ({
recordPickerInstanceId,
}: OpenActivityTargetInlineCellEditModeProps) => {
// eslint-disable-next-line no-console
console.log('openActivityTargetInlineCellEditMode', recordPickerInstanceId);
};
return { openActivityTargetInlineCellEditMode };
};