Refactor MultipleObjectsPicker component (#10552)

Refactor to only have MultipleRecordPicker and SingleRecordPicker

What's done:
- SingleRecordPicker, MultipleRecordPicker
- RelationToOneInput
- RelationFromManyInput
- usage in TableCell, InlineCell, RelationDetailSection, Workflow

What's left:
- Make a pass on the app, to make sure the hotkeyScopes, clickOutside
are properly set
- Fix flashing on ActivityTarget
- add more tests on the code
This commit is contained in:
Charles Bochet
2025-03-10 15:04:09 +01:00
committed by GitHub
parent 7eabcc8774
commit f0de6d31b7
126 changed files with 2465 additions and 2242 deletions

View File

@ -1,35 +0,0 @@
import { objectRecordsIdsMultiSelecComponentState } from '@/activities/states/objectRecordsIdsMultiSelectComponentState';
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
import { recordMultiSelectIsLoadingComponentState } from '@/object-record/record-field/states/recordMultiSelectIsLoadingComponentState';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useObjectRecordMultiSelectScopedStates = (scopeId: string) => {
const objectRecordsIdsMultiSelectState = extractComponentState(
objectRecordsIdsMultiSelecComponentState,
scopeId,
);
const objectRecordMultiSelectCheckedRecordsIdsState = extractComponentState(
objectRecordMultiSelectCheckedRecordsIdsComponentState,
scopeId,
);
const objectRecordMultiSelectFamilyState = extractComponentFamilyState(
objectRecordMultiSelectComponentFamilyState,
scopeId,
);
const recordMultiSelectIsLoadingState = extractComponentState(
recordMultiSelectIsLoadingComponentState,
scopeId,
);
return {
objectRecordsIdsMultiSelectState,
objectRecordMultiSelectCheckedRecordsIdsState,
objectRecordMultiSelectFamilyState,
recordMultiSelectIsLoadingState,
};
};

View File

@ -1,288 +0,0 @@
import { isNull } from '@sniptt/guards';
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';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
import {
ObjectRecordAndSelected,
objectRecordMultiSelectComponentFamilyState,
} from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
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;
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
};
export const ActivityTargetInlineCellEditMode = ({
activity,
activityTargetWithTargetRecords,
activityObjectNameSingular,
}: ActivityTargetInlineCellEditModeProps) => {
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const recordPickerInstanceId = `record-picker-${activity.id}`;
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
(activityTarget) => ({
objectNameSingular: activityTarget.targetObjectMetadataItem.nameSingular,
id: activityTarget.targetObject.id,
}),
);
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
NoteTarget | TaskTarget
>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const { closeInlineCell: closeEditableField } = useInlineCell();
const { upsertActivity } = useUpsertActivity({
activityObjectNameSingular,
});
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItem({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const setActivityFromStore = useSetRecoilState(
recordStoreFamilyState(activity.id),
);
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
useCreateManyRecordsInCache<NoteTarget | TaskTarget>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const handleSubmit = useRecoilCallback(
({ snapshot }) =>
async () => {
const activityTargetsAfterUpdate =
activityTargetWithTargetRecords.filter((activityTarget) => {
const recordSelectedInMultiSelect = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: recordPickerInstanceId,
familyKey: activityTarget.targetObject.id,
}),
)
.getValue() as ObjectRecordAndSelected;
return recordSelectedInMultiSelect
? recordSelectedInMultiSelect.selected
: true;
});
setActivityFromStore((currentActivity) => {
if (isNull(currentActivity)) {
return null;
}
return {
...currentActivity,
activityTargets: activityTargetsAfterUpdate,
};
});
closeEditableField();
},
[
activityTargetWithTargetRecords,
closeEditableField,
recordPickerInstanceId,
setActivityFromStore,
],
);
const handleChange = useRecoilCallback(
({ snapshot, set }) =>
async (recordId: string) => {
const existingActivityTargets = activityTargetWithTargetRecords.map(
(activityTargetObjectRecord) =>
activityTargetObjectRecord.activityTarget,
);
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
const previouslyCheckedRecordsIds = snapshot
.getLoadable(
objectRecordMultiSelectCheckedRecordsIdsComponentState({
scopeId: recordPickerInstanceId,
}),
)
.getValue();
const isNewlySelected = !previouslyCheckedRecordsIds.includes(recordId);
if (isNewlySelected) {
const record = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: recordPickerInstanceId,
familyKey: recordId,
}),
)
.getValue();
if (!record) {
throw new Error(
`Could not find selected record with id ${recordId}`,
);
}
set(
objectRecordMultiSelectCheckedRecordsIdsComponentState({
scopeId: recordPickerInstanceId,
}),
(prev) => [...prev, recordId],
);
const newActivityTargetId = v4();
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
nameSingular: record.objectMetadataItem.nameSingular,
});
const newActivityTargetInput = {
id: newActivityTargetId,
...(activityObjectNameSingular === CoreObjectNameSingular.Task
? { taskId: activity.id }
: { noteId: activity.id }),
[fieldNameWithIdSuffix]: recordId,
};
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
objectMetadataItem: objectMetadataItemActivityTarget,
input: newActivityTargetInput,
});
activityTargetsAfterUpdate.push(newActivityTarget);
if (isActivityInCreateMode) {
createManyActivityTargetsInCache([newActivityTarget]);
upsertActivity({
activity,
input: {
[activityObjectNameSingular === CoreObjectNameSingular.Task
? 'taskTargets'
: activityObjectNameSingular === CoreObjectNameSingular.Note
? 'noteTargets'
: '']: activityTargetsAfterUpdate,
},
});
} else {
await createOneActivityTarget(newActivityTargetInput);
}
set(activityTargetObjectRecordFamilyState(recordId), {
activityTargetId: newActivityTargetId,
});
} else {
const activityTargetToDeleteId = snapshot
.getLoadable(activityTargetObjectRecordFamilyState(recordId))
.getValue().activityTargetId;
if (!activityTargetToDeleteId) {
throw new Error('Could not delete this activity target.');
}
set(
objectRecordMultiSelectCheckedRecordsIdsComponentState({
scopeId: recordPickerInstanceId,
}),
previouslyCheckedRecordsIds.filter((id) => id !== recordId),
);
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
(activityTarget) => activityTarget.id !== activityTargetToDeleteId,
);
if (isActivityInCreateMode) {
upsertActivity({
activity,
input: {
[activityObjectNameSingular === CoreObjectNameSingular.Task
? 'taskTargets'
: activityObjectNameSingular === CoreObjectNameSingular.Note
? 'noteTargets'
: '']: activityTargetsAfterUpdate,
},
});
} else {
await deleteOneActivityTarget(activityTargetToDeleteId);
}
set(activityTargetObjectRecordFamilyState(recordId), {
activityTargetId: null,
});
}
},
[
activity,
activityTargetWithTargetRecords,
createOneActivityTarget,
createManyActivityTargetsInCache,
deleteOneActivityTarget,
isActivityInCreateMode,
objectMetadataItemActivityTarget,
recordPickerInstanceId,
upsertActivity,
activityObjectNameSingular,
],
);
const containerRef = useRef<HTMLDivElement>(null);
return (
<>
<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}
/>
</div>
</>
);
};

View File

@ -1,111 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,42 +0,0 @@
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const ActivityTargetObjectRecordEffect = ({
activityTargetWithTargetRecords,
}: {
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
}) => {
const updateActivityTargets = useRecoilCallback(
({ snapshot, set }) =>
(newActivityTargets: ActivityTargetWithTargetRecord[]) => {
for (const newActivityTarget of newActivityTargets) {
const objectRecordId = newActivityTarget.targetObject.id;
const record = snapshot
.getLoadable(activityTargetObjectRecordFamilyState(objectRecordId))
.getValue();
if (
!isDeeplyEqual(
record.activityTargetId,
newActivityTarget.activityTarget.id,
)
) {
set(activityTargetObjectRecordFamilyState(objectRecordId), {
activityTargetId: newActivityTarget.activityTarget.id,
});
}
}
},
[],
);
useEffect(() => {
updateActivityTargets(activityTargetWithTargetRecords);
}, [activityTargetWithTargetRecords, updateActivityTargets]);
return <></>;
};

View File

@ -4,8 +4,8 @@ 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 { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
@ -18,6 +18,7 @@ import { RecordFieldInputScope } from '@/object-record/record-field/scopes/Recor
import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
type ActivityTargetsInlineCellProps = {
@ -38,6 +39,8 @@ export const ActivityTargetsInlineCell = ({
const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(activity);
const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activity.id}`;
const { closeInlineCell } = useInlineCell();
const { fieldDefinition } = useContext(FieldContext);
@ -64,6 +67,12 @@ export const ActivityTargetsInlineCell = ({
const { openActivityTargetInlineCellEditMode } =
useOpenActivityTargetInlineCellEditMode();
const { updateActivityTargetFromInlineCell } =
useUpdateActivityTargetFromInlineCell({
activityObjectNameSingular,
activityId: activity.id,
});
return (
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
<FieldFocusContextProvider>
@ -72,20 +81,27 @@ export const ActivityTargetsInlineCell = ({
<RecordInlineCellContext.Provider
value={{
buttonIcon: IconPencil,
customEditHotkeyScope: {
scope: ActivityEditorHotkeyScope.ActivityTargets,
},
customEditHotkeyScope:
ActivityEditorHotkeyScope.ActivityTargets,
IconLabel: showLabel ? IconArrowUpRight : undefined,
showLabel: showLabel,
readonly: isFieldReadOnly,
labelWidth: fieldDefinition?.labelWidth,
editModeContent: (
<ActivityTargetInlineCellEditMode
activity={activity}
activityTargetWithTargetRecords={
activityTargetObjectRecords
}
activityObjectNameSingular={activityObjectNameSingular}
<MultipleRecordPicker
componentInstanceId={multipleRecordPickerInstanceId}
onClickOutside={() => {}}
onChange={(morphItem) => {
updateActivityTargetFromInlineCell({
recordPickerInstanceId: multipleRecordPickerInstanceId,
morphItem,
activityTargetWithTargetRecords:
activityTargetObjectRecords,
});
}}
onSubmit={() => {
closeInlineCell();
}}
/>
),
label: 'Relations',
@ -97,7 +113,8 @@ export const ActivityTargetsInlineCell = ({
),
onOpenEditMode: () => {
openActivityTargetInlineCellEditMode({
recordPickerInstanceId: `record-picker-${activity.id}`,
recordPickerInstanceId: multipleRecordPickerInstanceId,
activityTargetObjectRecords,
});
},
}}

View File

@ -1,40 +0,0 @@
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

@ -1,95 +0,0 @@
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

@ -1,97 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,62 +0,0 @@
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 { isDefined } from 'twenty-shared';
export const useMultiObjectSearch = ({
searchFilterValue,
limit,
excludedObjects,
}: {
searchFilterValue: string;
limit?: number;
excludedObjects?: CoreObjectNameSingular[];
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const selectableObjectMetadataItems = objectMetadataItems.filter(
({ nameSingular, isSearchable }) =>
!excludedObjects?.includes(nameSingular as CoreObjectNameSingular) &&
isSearchable,
);
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

@ -1,61 +0,0 @@
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

@ -1,100 +0,0 @@
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

@ -1,14 +1,90 @@
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilCallback } from 'recoil';
type OpenActivityTargetInlineCellEditModeProps = {
recordPickerInstanceId: string;
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
};
export const useOpenActivityTargetInlineCellEditMode = () => {
const openActivityTargetInlineCellEditMode = ({
recordPickerInstanceId,
}: OpenActivityTargetInlineCellEditModeProps) => {
// eslint-disable-next-line no-console
console.log('openActivityTargetInlineCellEditMode', recordPickerInstanceId);
};
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch();
const openActivityTargetInlineCellEditMode = useRecoilCallback(
({ set, snapshot }) =>
({
recordPickerInstanceId,
activityTargetObjectRecords,
}: OpenActivityTargetInlineCellEditModeProps) => {
const objectMetadataItems = snapshot
.getLoadable(objectMetadataItemsState)
.getValue()
.filter(
(objectMetadataItem) =>
objectMetadataItem.isSearchable &&
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Task &&
objectMetadataItem.nameSingular !== CoreObjectNameSingular.Note,
);
set(
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
instanceId: recordPickerInstanceId,
}),
activityTargetObjectRecords.map((activityTargetObjectRecord) => ({
recordId: activityTargetObjectRecord.targetObject.id,
objectMetadataId:
activityTargetObjectRecord.targetObjectMetadataItem.id,
isSelected: true,
isMatchingSearchFilter: true,
})),
);
set(
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
{
instanceId: recordPickerInstanceId,
},
),
objectMetadataItems,
);
set(
multipleRecordPickerSearchFilterComponentState.atomFamily({
instanceId: recordPickerInstanceId,
}),
'',
);
toggleRightDrawerClickOustideListener(false);
multipleRecordPickerPerformSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId,
forceSearchFilter: '',
forceSearchableObjectMetadataItems: objectMetadataItems,
forcePickableMorphItems: activityTargetObjectRecords.map(
(activityTargetObjectRecord) => ({
recordId: activityTargetObjectRecord.targetObject.id,
objectMetadataId:
activityTargetObjectRecord.targetObjectMetadataItem.id,
isSelected: true,
isMatchingSearchFilter: true,
}),
),
});
},
[multipleRecordPickerPerformSearch, toggleRightDrawerClickOustideListener],
);
return { openActivityTargetInlineCellEditMode };
};

View File

@ -0,0 +1,173 @@
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isNull } from '@sniptt/guards';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
type UpdateActivityTargetFromInlineCellProps = {
recordPickerInstanceId: string;
morphItem: RecordPickerPickableMorphItem;
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
};
export const useUpdateActivityTargetFromInlineCell = ({
activityObjectNameSingular,
activityId,
}: {
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
activityId: string;
}) => {
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
NoteTarget | TaskTarget
>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const setActivityFromStore = useSetRecoilState(
recordStoreFamilyState(activityId),
);
const updateActivityTargetFromInlineCell = useRecoilCallback(
({ snapshot }) =>
async ({
morphItem,
activityTargetWithTargetRecords,
}: UpdateActivityTargetFromInlineCellProps) => {
const targetObjectName =
activityObjectNameSingular === CoreObjectNameSingular.Task
? 'task'
: 'note';
const pickedObjectMetadataItem = snapshot
.getLoadable(objectMetadataItemsState)
.getValue()
.find(
(objectMetadataItem) =>
objectMetadataItem.id === morphItem.objectMetadataId,
);
if (!isDefined(pickedObjectMetadataItem)) {
throw new Error('Could not find object metadata item');
}
let activityTargetsAfterUpdate: (TaskTarget | NoteTarget)[] = [];
const existingActivityTarget = activityTargetWithTargetRecords.find(
(activityTarget) =>
activityTarget.targetObject.id === morphItem.recordId,
);
if (isDefined(existingActivityTarget)) {
activityTargetsAfterUpdate = activityTargetWithTargetRecords
.map((activityTarget) => {
if (
activityTarget.targetObject.id === morphItem.recordId &&
!morphItem.isSelected
) {
return undefined;
}
return activityTarget.activityTarget;
})
.filter(isDefined);
if (!morphItem.isSelected) {
await deleteOneActivityTarget(
existingActivityTarget.targetObject.id,
);
}
} else {
const targetRecord = snapshot
.getLoadable(recordStoreFamilyState(morphItem.recordId))
.getValue();
if (!isDefined(targetRecord)) {
return;
}
if (!morphItem.isSelected) {
return;
}
const activityTarget =
activityObjectNameSingular === CoreObjectNameSingular.Task
? {
id: v4(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
__typename: 'TaskTarget',
taskId: activityId,
task: {
id: activityId,
__typename: 'Task',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
[pickedObjectMetadataItem.nameSingular]: targetRecord,
}
: {
id: v4(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
__typename: 'NoteTarget',
noteId: activityId,
note: {
id: activityId,
__typename: 'Note',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
[pickedObjectMetadataItem.nameSingular]: targetRecord,
};
activityTargetsAfterUpdate = [
...activityTargetWithTargetRecords.map((activityTarget) => {
return activityTarget.activityTarget;
}),
activityTarget as NoteTarget | TaskTarget,
];
await createOneActivityTarget({
...activityTarget,
[targetObjectName]: undefined,
[pickedObjectMetadataItem.nameSingular]: undefined,
} as Partial<NoteTarget | TaskTarget>);
}
setActivityFromStore((currentActivity) => {
if (isNull(currentActivity)) {
return null;
}
return {
...currentActivity,
[`${targetObjectName}Targets`]: activityTargetsAfterUpdate,
};
});
},
[
activityId,
activityObjectNameSingular,
createOneActivityTarget,
deleteOneActivityTarget,
setActivityFromStore,
],
);
return { updateActivityTargetFromInlineCell };
};