Files
twenty/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
Charles Bochet d246010c5c 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
2025-02-27 09:56:03 +01:00

289 lines
11 KiB
TypeScript

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>
</>
);
};