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:
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
}}
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user