Files
twenty_crm/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
Marie ac233b771c Simplify multi-object picker logic with search (#8010)
Simplifying the logic around multi-object pickers and search by getting
rid of the behaviour that keeped selected elements even when they did
not match the search filter (eg keeping selected record "Brian Chesky"
in dropdown even when search input is "Qonto"). This allows us to
simplify the fetch queries around the search to only do one query.

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2024-11-07 17:09:19 +01:00

297 lines
11 KiB
TypeScript

import styled from '@emotion/styled';
import { isNull } from '@sniptt/guards';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
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 { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect';
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { prefillRecord } from '@/object-record/utils/prefillRecord';
const StyledSelectContainer = styled.div`
position: absolute;
left: 0;
top: 0;
`;
type ActivityTargetInlineCellEditModeProps = {
activity: Task | Note;
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
};
export const ActivityTargetInlineCellEditMode = ({
activity,
activityTargetWithTargetRecords,
activityObjectNameSingular,
}: ActivityTargetInlineCellEditModeProps) => {
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const relationPickerScopeId = `relation-picker-${activity.id}`;
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
(activityTarget) => ({
objectNameSingular: activityTarget.targetObjectMetadataItem.nameSingular,
id: activityTarget.targetObject.id,
}),
);
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
NoteTarget | TaskTarget
>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
{
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 record = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: relationPickerScopeId,
familyKey: activityTarget.targetObject.id,
}),
)
.getValue() as ObjectRecordAndSelected;
return record.selected;
});
setActivityFromStore((currentActivity) => {
if (isNull(currentActivity)) {
return null;
}
return {
...currentActivity,
activityTargets: activityTargetsAfterUpdate,
};
});
closeEditableField();
},
[
activityTargetWithTargetRecords,
closeEditableField,
relationPickerScopeId,
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: relationPickerScopeId,
}),
)
.getValue();
const isNewlySelected = !previouslyCheckedRecordsIds.includes(recordId);
if (isNewlySelected) {
const record = snapshot
.getLoadable(
objectRecordMultiSelectComponentFamilyState({
scopeId: relationPickerScopeId,
familyKey: recordId,
}),
)
.getValue();
if (!record) {
throw new Error(
`Could not find selected record with id ${recordId}`,
);
}
set(
objectRecordMultiSelectCheckedRecordsIdsComponentState({
scopeId: relationPickerScopeId,
}),
(prev) => [...prev, recordId],
);
const newActivityTargetId = v4();
const fieldName = record.objectMetadataItem.nameSingular;
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
nameSingular: record.objectMetadataItem.nameSingular,
});
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
objectMetadataItem: objectMetadataItemActivityTarget,
input: {
id: newActivityTargetId,
taskId:
activityObjectNameSingular === CoreObjectNameSingular.Task
? activity.id
: null,
task:
activityObjectNameSingular === CoreObjectNameSingular.Task
? activity
: null,
noteId:
activityObjectNameSingular === CoreObjectNameSingular.Note
? activity.id
: null,
note:
activityObjectNameSingular === CoreObjectNameSingular.Note
? activity
: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
[fieldName]: record.record,
[fieldNameWithIdSuffix]: recordId,
},
});
activityTargetsAfterUpdate.push(newActivityTarget);
if (isActivityInCreateMode) {
createManyActivityTargetsInCache([newActivityTarget]);
upsertActivity({
activity,
input: {
[activityObjectNameSingular === CoreObjectNameSingular.Task
? 'taskTargets'
: activityObjectNameSingular === CoreObjectNameSingular.Note
? 'noteTargets'
: '']: activityTargetsAfterUpdate,
},
});
} else {
await createManyActivityTargets([newActivityTarget]);
}
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: relationPickerScopeId,
}),
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 deleteManyActivityTargets([activityTargetToDeleteId]);
}
set(activityTargetObjectRecordFamilyState(recordId), {
activityTargetId: null,
});
}
},
[
activity,
activityTargetWithTargetRecords,
createManyActivityTargets,
createManyActivityTargetsInCache,
deleteManyActivityTargets,
isActivityInCreateMode,
objectMetadataItemActivityTarget,
relationPickerScopeId,
upsertActivity,
activityObjectNameSingular,
],
);
return (
<StyledSelectContainer>
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
<ActivityTargetObjectRecordEffect
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
/>
<ActivityTargetInlineCellEditModeMultiRecordsEffect
selectedObjectRecordIds={selectedTargetObjectIds}
/>
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
</RelationPickerScope>
</StyledSelectContainer>
);
};