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 };
|
||||
};
|
||||
@ -3,13 +3,13 @@ import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
|
||||
const instanceId = 'instanceId';
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
|
||||
describe('useLimitPerMetadataItem', () => {
|
||||
|
||||
@ -17,7 +17,7 @@ export const getLabelIdentifierFieldValue = (
|
||||
}
|
||||
|
||||
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
|
||||
return String(record[labelIdentifierFieldMetadataItem.name]);
|
||||
return record[labelIdentifierFieldMetadataItem.name];
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@ -12,7 +12,13 @@ export const getObjectRecordIdentifier = ({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
| 'fields'
|
||||
| 'labelIdentifierFieldMetadataId'
|
||||
| 'nameSingular'
|
||||
| 'imageIdentifierFieldMetadataId'
|
||||
>;
|
||||
record: ObjectRecord;
|
||||
}): ObjectRecordIdentifier => {
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
|
||||
@ -5,7 +5,7 @@ import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const useCombinedFindManyRecords = ({
|
||||
operationSignatures,
|
||||
@ -22,7 +22,7 @@ export const useCombinedFindManyRecords = ({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const { data, loading } = useQuery<MultiObjectRecordQueryResult>(
|
||||
const { data, loading } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const useCombinedGetTotalCount = ({
|
||||
objectMetadataItems,
|
||||
@ -28,7 +28,7 @@ export const useCombinedGetTotalCount = ({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const { data } = useQuery<MultiObjectRecordQueryResult>(
|
||||
const { data } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
|
||||
export const useGenerateCombinedSearchRecordsQuery = ({
|
||||
@ -20,70 +16,8 @@ export const useGenerateCombinedSearchRecordsQuery = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||
objectNameSingular,
|
||||
)}FilterInput`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
|
||||
const filteredQueryKeyWithObjectMetadataItemArray =
|
||||
queryKeyWithObjectMetadataItemArray.filter(
|
||||
({ objectMetadataItem }) => objectMetadataItem.isSearchable,
|
||||
);
|
||||
|
||||
return gql`
|
||||
query CombinedSearchRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray},
|
||||
$search: String,
|
||||
) {
|
||||
${filteredQueryKeyWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem }) =>
|
||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)},
|
||||
searchInput: $search
|
||||
){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
}`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
return generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems,
|
||||
operationSignatures,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
|
||||
export type MultiObjectRecordQueryResult = {
|
||||
export type CombinedFindManyRecordsQueryResult = {
|
||||
[namePlural: string]: RecordGqlConnection;
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult';
|
||||
|
||||
export const formatMultiObjectRecordSearchResults = (
|
||||
searchResults: MultiObjectRecordQueryResult | undefined | null,
|
||||
): MultiObjectRecordQueryResult => {
|
||||
if (!searchResults) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(searchResults).reduce((acc, [key, value]) => {
|
||||
let newKey = key.replace(/^search/, '');
|
||||
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
||||
acc[newKey] = value;
|
||||
return acc;
|
||||
}, {} as MultiObjectRecordQueryResult);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import gql from 'graphql-tag';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
|
||||
export const generateCombinedSearchRecordsQuery = ({
|
||||
objectMetadataItems,
|
||||
operationSignatures,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
operationSignatures: RecordGqlOperationSignature[];
|
||||
}) => {
|
||||
const filterPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||
objectNameSingular,
|
||||
)}FilterInput`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
|
||||
const filteredQueryKeyWithObjectMetadataItemArray =
|
||||
queryKeyWithObjectMetadataItemArray.filter(
|
||||
({ objectMetadataItem }) => objectMetadataItem.isSearchable,
|
||||
);
|
||||
|
||||
return gql`
|
||||
query CombinedSearchRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray},
|
||||
$search: String,
|
||||
) {
|
||||
${filteredQueryKeyWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem }) =>
|
||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)},
|
||||
searchInput: $search
|
||||
){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
}`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
};
|
||||
@ -7,8 +7,8 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM
|
||||
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
@ -91,7 +91,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
|
||||
<SelectableList
|
||||
selectableListId="boolean-select"
|
||||
selectableItemIdArray={options.map((option) => option.toString())}
|
||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
onEnter={(itemId) => {
|
||||
handleOptionSelect(itemId === 'true');
|
||||
}}
|
||||
|
||||
@ -15,7 +15,7 @@ import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/i
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
@ -87,7 +87,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const defaultOperand = getRecordFilterOperands({
|
||||
|
||||
@ -18,9 +18,8 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o
|
||||
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
|
||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -102,7 +101,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
closeDropdown();
|
||||
resetSelectedItem();
|
||||
},
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
[closeDropdown, resetSelectedItem],
|
||||
);
|
||||
|
||||
@ -165,7 +164,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
||||
<SelectableList
|
||||
selectableListId={componentInstanceId}
|
||||
selectableItemIdArray={objectRecordsIds}
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
onEnter={(itemId) => {
|
||||
const option = optionsInDropdown.find((option) => option.id === itemId);
|
||||
if (isDefined(option)) {
|
||||
|
||||
@ -12,7 +12,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop
|
||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
@ -232,7 +232,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
||||
)}
|
||||
<MultipleSelectDropdown
|
||||
selectableListId="object-filter-record-select-id"
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
itemsToSelect={recordsToSelect}
|
||||
filteredSelectedItems={filteredSelectedRecords}
|
||||
selectedItems={selectedRecords}
|
||||
|
||||
@ -12,7 +12,7 @@ import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -144,7 +144,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
||||
return (
|
||||
<MultipleSelectDropdown
|
||||
selectableListId="object-filter-source-select-id"
|
||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
||||
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||
itemsToSelect={sourceTypes.filter(
|
||||
(item) =>
|
||||
!filteredSelectedItems.some((selected) => selected.id === item.id),
|
||||
|
||||
@ -8,7 +8,7 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object-
|
||||
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -67,7 +67,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => {
|
||||
fieldMetadataItem.type === 'RELATION' ||
|
||||
fieldMetadataItem.type === 'SELECT'
|
||||
) {
|
||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
||||
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||
}
|
||||
|
||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||
|
||||
@ -2,8 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
@ -68,22 +67,15 @@ export const RecordBoardColumnNewOpportunity = ({
|
||||
<>
|
||||
{newRecord.isCreating && newRecord.position === position && (
|
||||
<OverlayContainer>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: `add-new-card-record-picker-column-${columnId}`,
|
||||
}}
|
||||
>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
selectedRecordIds={[]}
|
||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||
onRecordSelected={(company) =>
|
||||
company ? handleEntitySelect(position, company) : null
|
||||
}
|
||||
objectNameSingular={CoreObjectNameSingular.Company}
|
||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||
/>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { RecoilState, useRecoilCallback } from 'recoil';
|
||||
@ -26,7 +26,7 @@ export const useAddNewCard = ({
|
||||
const columnContext = useContext(RecordBoardColumnContext);
|
||||
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
|
||||
useContext(RecordBoardContext);
|
||||
const { resetSearchFilter } = useRecordSelectSearch(
|
||||
const { resetSearchFilter } = useSingleRecordPickerSearch(
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
|
||||
@ -139,7 +139,7 @@ export const useAddNewCard = ({
|
||||
addNewItem(set, columnDefinitionId, position, isOpportunity);
|
||||
if (isOpportunity) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
);
|
||||
} else {
|
||||
createRecord(labelIdentifier, labelValue, position, isOpportunity);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export type NewCard = {
|
||||
|
||||
@ -17,6 +17,7 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
|
||||
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||
@ -71,108 +72,114 @@ export const FieldInput = ({
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
return (
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: recordFieldInputdId,
|
||||
}}
|
||||
>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
<PhonesFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldEmails(fieldDefinition) ? (
|
||||
<EmailsFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateTimeFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldInput onCancel={onCancel} />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
<RawJsonFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldArray(fieldDefinition) ? (
|
||||
<ArrayFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</RecordFieldInputScope>
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||
>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
<PhonesFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldText(fieldDefinition) ? (
|
||||
<TextFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldEmails(fieldDefinition) ? (
|
||||
<EmailsFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldFullName(fieldDefinition) ? (
|
||||
<FullNameFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldDateTime(fieldDefinition) ? (
|
||||
<DateTimeFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldDate(fieldDefinition) ? (
|
||||
<DateFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onClear={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
) : isFieldNumber(fieldDefinition) ? (
|
||||
<NumberFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldLinks(fieldDefinition) ? (
|
||||
<LinksFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : isFieldCurrency(fieldDefinition) ? (
|
||||
<CurrencyFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldSelect(fieldDefinition) ? (
|
||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||
<MultiSelectFieldInput onCancel={onCancel} />
|
||||
) : isFieldAddress(fieldDefinition) ? (
|
||||
<AddressFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldRawJson(fieldDefinition) ? (
|
||||
<RawJsonFieldInput
|
||||
onEnter={onEnter}
|
||||
onEscape={onEscape}
|
||||
onClickOutside={onClickOutside}
|
||||
onTab={onTab}
|
||||
onShiftTab={onShiftTab}
|
||||
/>
|
||||
) : isFieldArray(fieldDefinition) ? (
|
||||
<ArrayFieldInput
|
||||
onCancel={onCancel}
|
||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</RecordFieldInputScope>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
|
||||
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useOpenFieldInputEditMode = () => {
|
||||
const { openRelationToOneFieldInput } = useOpenRelationToOneFieldInput();
|
||||
const { openRelationFromManyFieldInput } =
|
||||
useOpenRelationFromManyFieldInput();
|
||||
|
||||
const openFieldInput = ({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
}: {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
}) => {
|
||||
if (isFieldRelationToOneObject(fieldDefinition)) {
|
||||
openRelationToOneFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||
if (
|
||||
isDefined(fieldDefinition.metadata.relationObjectMetadataNameSingular)
|
||||
) {
|
||||
openRelationFromManyFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
openFieldInput: openFieldInput,
|
||||
closeFieldInput: () => {},
|
||||
};
|
||||
};
|
||||
@ -31,7 +31,6 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
@ -156,13 +155,12 @@ export const usePersistField = () => {
|
||||
);
|
||||
|
||||
if (fieldIsRelationToOneObject) {
|
||||
const value = valueToPersist as SingleRecordPickerRecord;
|
||||
updateRecord?.({
|
||||
variables: {
|
||||
where: { id: recordId },
|
||||
updateOneRecordInput: {
|
||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
||||
value?.id ?? null,
|
||||
valueToPersist?.id ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -7,14 +7,12 @@ import { FieldRelationValue } from '@/object-record/record-field/types/FieldMeta
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldContext } from '../../contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
export const useRelationField = <
|
||||
T extends SingleRecordPickerRecord | SingleRecordPickerRecord[],
|
||||
>() => {
|
||||
export const useRelationField = <T extends ObjectRecord | ObjectRecord[]>() => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
const button = useGetButtonIcon();
|
||||
|
||||
|
||||
@ -2,14 +2,14 @@ import { useContext } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
type RelationFromManyFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
@ -19,10 +19,9 @@ export const RelationFromManyFieldInput = ({
|
||||
onSubmit,
|
||||
}: RelationFromManyFieldInputProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const recordPickerInstanceId = `record-picker-${fieldDefinition.fieldMetadataId}`;
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput({
|
||||
scopeId: recordPickerInstanceId,
|
||||
});
|
||||
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.(() => {});
|
||||
@ -50,19 +49,22 @@ export const RelationFromManyFieldInput = ({
|
||||
recordId,
|
||||
});
|
||||
|
||||
const layoutDirection = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordPickerInstanceId }}
|
||||
>
|
||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={updateRelation}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
/>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</>
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={updateRelation}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onClickOutside={handleSubmit}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
? 'search-bar-on-top'
|
||||
: 'search-bar-on-bottom'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const RelationFromManyFieldInputMultiRecordsEffect = () => {
|
||||
const { fieldValue, fieldDefinition } =
|
||||
useRelationField<SingleRecordPickerRecord[]>();
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
const {
|
||||
objectRecordsIdsMultiSelectState,
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
recordMultiSelectIsLoadingState,
|
||||
} = useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
|
||||
useRecoilState(objectRecordsIdsMultiSelectState);
|
||||
|
||||
const { records } = useRecordPickerRecordsOptions({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const setRecordMultiSelectIsLoading = useSetRecoilState(
|
||||
recordMultiSelectIsLoadingState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const allRecords = useMemo(
|
||||
() => [
|
||||
...records.recordsToSelect.map((entity) => {
|
||||
const { record, ...recordIdentifier } = entity;
|
||||
return {
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
record: record,
|
||||
recordIdentifier: recordIdentifier,
|
||||
};
|
||||
}),
|
||||
],
|
||||
[records.recordsToSelect, objectMetadataItem],
|
||||
);
|
||||
|
||||
const [
|
||||
objectRecordMultiSelectCheckedRecordsIds,
|
||||
setObjectRecordMultiSelectCheckedRecordsIds,
|
||||
] = useRecoilState(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 newRecordWithSelected = {
|
||||
...newRecord,
|
||||
selected: objectRecordMultiSelectCheckedRecordsIds.includes(
|
||||
newRecord.record.id,
|
||||
),
|
||||
};
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
newRecordWithSelected.selected,
|
||||
currentRecord?.selected,
|
||||
)
|
||||
) {
|
||||
set(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: instanceId,
|
||||
familyKey: newRecordWithSelected.record.id,
|
||||
}),
|
||||
newRecordWithSelected,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[objectRecordMultiSelectCheckedRecordsIds, instanceId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateRecords(allRecords);
|
||||
const allRecordsIds = allRecords.map((record) => record.record.id);
|
||||
if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) {
|
||||
setObjectRecordsIdsMultiSelect(allRecordsIds);
|
||||
}
|
||||
}, [
|
||||
allRecords,
|
||||
objectRecordsIdsMultiSelect,
|
||||
setObjectRecordsIdsMultiSelect,
|
||||
updateRecords,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectRecordMultiSelectCheckedRecordsIds(
|
||||
fieldValue
|
||||
? fieldValue.map(
|
||||
(fieldValueItem: SingleRecordPickerRecord) => fieldValueItem.id,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}, [fieldValue, setObjectRecordMultiSelectCheckedRecordsIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordMultiSelectIsLoading(records.loading);
|
||||
}, [records.loading, setRecordMultiSelectIsLoading]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,83 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { IconForbid } from 'twenty-ui';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationPickerInitialValueEffect } from '@/object-record/record-field/meta-types/input/components/RelationPickerInitialValueEffect';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
export type RelationPickerProps = {
|
||||
selectedRecordId?: string;
|
||||
onSubmit: (selectedRecord: SingleRecordPickerRecord | null) => void;
|
||||
onCancel?: () => void;
|
||||
width?: number;
|
||||
excludedRecordIds?: string[];
|
||||
initialSearchFilter?: string | null;
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
|
||||
};
|
||||
|
||||
export const RelationPicker = ({
|
||||
selectedRecordId,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
excludedRecordIds,
|
||||
width,
|
||||
initialSearchFilter,
|
||||
fieldDefinition,
|
||||
}: RelationPickerProps) => {
|
||||
const recordPickerInstanceId = RelationPickerHotkeyScope.RelationPicker;
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit(selectedRecord ?? null);
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const { recordId } = useContext(FieldContext);
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
relationObjectMetadataNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RelationPickerInitialValueEffect
|
||||
initialValueForSearchFilter={initialSearchFilter}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
/>
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
width={width}
|
||||
objectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
selectedRecordIds={selectedRecordId ? [selectedRecordId] : []}
|
||||
excludedRecordIds={excludedRecordIds}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// Todo: this effect should be deprecated to use sync hooks
|
||||
export const RelationPickerInitialValueEffect = ({
|
||||
initialValueForSearchFilter,
|
||||
recordPickerInstanceId,
|
||||
}: {
|
||||
initialValueForSearchFilter?: string | null;
|
||||
recordPickerInstanceId: string;
|
||||
}) => {
|
||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
recordPickerInstanceId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordPickerSearchFilter(initialValueForSearchFilter ?? '');
|
||||
}, [initialValueForSearchFilter, setRecordPickerSearchFilter]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,8 +1,15 @@
|
||||
import { RelationPicker } from '@/object-record/record-field/meta-types/input/components/RelationPicker';
|
||||
import { usePersistField } from '../../../hooks/usePersistField';
|
||||
import { useRelationField } from '../../hooks/useRelationField';
|
||||
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { IconForbid } from 'twenty-ui';
|
||||
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||
|
||||
export type RelationToOneFieldInputProps = {
|
||||
@ -14,22 +21,64 @@ export const RelationToOneFieldInput = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationToOneFieldInputProps) => {
|
||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
||||
useRelationField<SingleRecordPickerRecord>();
|
||||
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const handleSubmit = (newEntity: SingleRecordPickerRecord | null) => {
|
||||
onSubmit?.(() => persistField(newEntity?.record ?? null));
|
||||
};
|
||||
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldDefinition.metadata.fieldName}`;
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => onSubmit?.(() => persistField(selectedRecord?.record ?? null));
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
relationObjectMetadataNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
recordId,
|
||||
});
|
||||
|
||||
const layoutDirection = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
);
|
||||
|
||||
const isLoading = useRecoilComponentValueV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<RelationPicker
|
||||
fieldDefinition={fieldDefinition}
|
||||
selectedRecordId={fieldValue?.id}
|
||||
onSubmit={handleSubmit}
|
||||
<SingleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
initialSearchFilter={initialSearchValue}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
recordPickerInstanceId={recordPickerInstanceId}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
? 'search-bar-on-top'
|
||||
: 'search-bar-on-bottom'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,7 +18,10 @@ import {
|
||||
} from '~/testing/mock-data/users';
|
||||
|
||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui';
|
||||
import {
|
||||
RelationToOneFieldInput,
|
||||
@ -30,11 +33,21 @@ const RelationWorkspaceSetterEffect = () => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setRecordFieldInputLayoutDirectionLoading =
|
||||
useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
'relation-to-one-field-input-123-Relation',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentWorkspace(mockCurrentWorkspace);
|
||||
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
|
||||
}, [setCurrentWorkspace, setCurrentWorkspaceMember]);
|
||||
setRecordFieldInputLayoutDirectionLoading(false);
|
||||
}, [
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setRecordFieldInputLayoutDirectionLoading,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -74,12 +87,18 @@ const RelationToOneFieldInputWithContext = ({
|
||||
}}
|
||||
recordId={recordId}
|
||||
>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'relation-to-one-field-input' }}
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: 'relation-to-one-field-input-123-Relation',
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
<SingleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'relation-to-one-field-input-123-Relation' }}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
</FieldContextProvider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
@ -10,9 +11,11 @@ import { viewableRecordIdState } from '@/object-record/record-right-drawer/state
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconEye } from 'twenty-ui';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@ -45,6 +48,10 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
||||
});
|
||||
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const { openRecordInCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
if (
|
||||
relationObjectMetadataNameSingular === 'workspaceMember' ||
|
||||
@ -110,10 +117,18 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
||||
|
||||
setViewableRecordId(newRecordId);
|
||||
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
|
||||
openRightDrawer(RightDrawerPages.ViewRecord, {
|
||||
title: 'View Record',
|
||||
Icon: IconEye,
|
||||
});
|
||||
|
||||
if (isCommandMenuEnabled) {
|
||||
openRecordInCommandMenu({
|
||||
recordId: newRecordId,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
} else {
|
||||
openRightDrawer(RightDrawerPages.ViewRecord, {
|
||||
title: 'View Record',
|
||||
Icon: IconEye,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import {
|
||||
FieldRelationFromManyValue,
|
||||
FieldRelationValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
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 { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useOpenRelationFromManyFieldInput = () => {
|
||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const openRelationFromManyFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
fieldName,
|
||||
objectNameSingular,
|
||||
recordId,
|
||||
}: {
|
||||
fieldName: string;
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
}) => {
|
||||
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationFromManyValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pickableMorphItems: RecordPickerPickableMorphItem[] =
|
||||
fieldValue.map((record) => {
|
||||
return {
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
};
|
||||
});
|
||||
|
||||
for (const record of fieldValue) {
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
}
|
||||
|
||||
set(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
}),
|
||||
pickableMorphItems,
|
||||
);
|
||||
|
||||
set(
|
||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
||||
{ instanceId: recordPickerInstanceId },
|
||||
),
|
||||
[objectMetadataItem],
|
||||
);
|
||||
|
||||
performSearch({
|
||||
multipleRecordPickerInstanceId: recordPickerInstanceId,
|
||||
forceSearchFilter: '',
|
||||
forceSearchableObjectMetadataItems: [objectMetadataItem],
|
||||
forcePickableMorphItems: pickableMorphItems,
|
||||
});
|
||||
},
|
||||
[performSearch],
|
||||
);
|
||||
|
||||
return { openRelationFromManyFieldInput };
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import {
|
||||
FieldRelationToOneValue,
|
||||
FieldRelationValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const useOpenRelationToOneFieldInput = () => {
|
||||
const openRelationToOneFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({ fieldName, recordId }: { fieldName: string; recordId: string }) => {
|
||||
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldName}`;
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationToOneValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (isDefined(fieldValue)) {
|
||||
set(
|
||||
singleRecordPickerSelectedIdComponentState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
}),
|
||||
fieldValue.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { openRelationToOneFieldInput };
|
||||
};
|
||||
@ -4,16 +4,12 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord';
|
||||
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateRelationFromManyFieldInput = ({
|
||||
scopeId,
|
||||
}: {
|
||||
scopeId: string;
|
||||
}) => {
|
||||
export const useUpdateRelationFromManyFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
@ -41,49 +37,21 @@ export const useUpdateRelationFromManyFieldInput = ({
|
||||
});
|
||||
|
||||
const updateRelation = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (objectRecordId: string) => {
|
||||
const previouslyCheckedRecordsIds = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const isNewlySelected =
|
||||
!previouslyCheckedRecordsIds.includes(objectRecordId);
|
||||
if (isNewlySelected) {
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
(prev) => [...prev, objectRecordId],
|
||||
);
|
||||
} else {
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId,
|
||||
}),
|
||||
(prev) => prev.filter((id) => id !== objectRecordId),
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewlySelected) {
|
||||
await updateOneRecordAndAttachRelations({
|
||||
recordId,
|
||||
relatedRecordId: objectRecordId,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachRelations({
|
||||
recordId,
|
||||
relatedRecordId: objectRecordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
() => async (morphItem: RecordPickerPickableMorphItem) => {
|
||||
if (morphItem.isSelected) {
|
||||
await updateOneRecordAndAttachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
recordId,
|
||||
scopeId,
|
||||
updateOneRecordAndAttachRelations,
|
||||
updateOneRecordAndDetachRelations,
|
||||
],
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export enum RelationPickerHotkeyScope {
|
||||
RelationPicker = 'relation-picker',
|
||||
AddNew = 'add-new',
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export type ActivityTargetObjectRecord = {
|
||||
activityTargetId: string | null;
|
||||
};
|
||||
|
||||
export const activityTargetObjectRecordFamilyState = createFamilyState<
|
||||
ActivityTargetObjectRecord,
|
||||
string
|
||||
>({
|
||||
key: 'activityTargetObjectRecordFamilyState',
|
||||
defaultValue: { activityTargetId: null },
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { ComponentStateKeyV2 } from '@/ui/utilities/state/component-state/types/ComponentStateKeyV2';
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
type RecordFieldComponentInstanceContextProps = ComponentStateKeyV2;
|
||||
|
||||
export const RecordFieldComponentInstanceContext =
|
||||
createComponentInstanceContext<RecordFieldComponentInstanceContextProps>();
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const objectRecordMultiSelectCheckedRecordsIdsComponentState =
|
||||
createComponentState<string[]>({
|
||||
key: 'objectRecordMultiSelectCheckedRecordsIdsComponentState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||
|
||||
export type ObjectRecordAndSelected = ObjectRecordForSelect & {
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export const objectRecordMultiSelectComponentFamilyState =
|
||||
createComponentFamilyState<ObjectRecordAndSelected | undefined, string>({
|
||||
key: 'objectRecordMultiSelectComponentFamilyState',
|
||||
defaultValue: undefined,
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const objectRecordMultiSelectMatchesFilterRecordsIdsComponentState =
|
||||
createComponentState<ObjectRecordForSelect[]>({
|
||||
key: 'objectRecordMultiSelectMatchesFilterRecordsIdsComponentState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { FieldInputLayoutDirection } from '@/object-record/record-field/types/FieldInputLayoutDirection';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordFieldInputLayoutDirectionComponentState =
|
||||
createComponentStateV2<FieldInputLayoutDirection>({
|
||||
key: 'recordFieldInputLayoutDirectionComponentState',
|
||||
defaultValue: 'upward',
|
||||
componentInstanceContext: RecordFieldComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const recordFieldInputLayoutDirectionLoadingComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'recordFieldInputLayoutDirectionLoadingComponentState',
|
||||
defaultValue: true,
|
||||
componentInstanceContext: RecordFieldComponentInstanceContext,
|
||||
});
|
||||
@ -1,7 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const recordMultiSelectIsLoadingComponentState =
|
||||
createComponentState<boolean>({
|
||||
key: 'recordMultiSelectIsLoadingComponentState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export type FieldInputLayoutDirection = 'upward' | 'downward';
|
||||
@ -2,7 +2,7 @@ import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared';
|
||||
import * as z from 'zod';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
@ -260,9 +260,9 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number] | null;
|
||||
export type FieldSelectValue = string | null;
|
||||
export type FieldMultiSelectValue = string[] | null;
|
||||
|
||||
export type FieldRelationToOneValue = SingleRecordPickerRecord | null;
|
||||
export type FieldRelationToOneValue = ObjectRecord | null;
|
||||
|
||||
export type FieldRelationFromManyValue = SingleRecordPickerRecord[] | [];
|
||||
export type FieldRelationFromManyValue = ObjectRecord[];
|
||||
|
||||
export type FieldRelationValue<
|
||||
T extends FieldRelationToOneValue | FieldRelationFromManyValue,
|
||||
|
||||
@ -2,10 +2,10 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie
|
||||
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata } from '../FieldMetadata';
|
||||
import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelationFromManyObjects = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldMetadata> =>
|
||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||
isFieldRelation(field) &&
|
||||
field.metadata.relationType === RelationDefinitionType.ONE_TO_MANY;
|
||||
|
||||
@ -8,17 +8,24 @@ import { FieldFocusContextProvider } from '@/object-record/record-field/contexts
|
||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
|
||||
import { useInlineCell } from '../hooks/useInlineCell';
|
||||
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
|
||||
import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput';
|
||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
||||
import {
|
||||
RecordInlineCellContext,
|
||||
@ -39,6 +46,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
onOpenEditMode,
|
||||
onCloseEditMode,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const buttonIcon = useGetButtonIcon();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
@ -101,13 +109,40 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
|
||||
|
||||
// TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope
|
||||
const computedHotkeyScope = (
|
||||
columnDefinition: FieldDefinition<FieldMetadata>,
|
||||
) => {
|
||||
if (isFieldRelation(columnDefinition)) {
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.MANY_TO_ONE
|
||||
) {
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (
|
||||
columnDefinition.metadata.relationType ===
|
||||
RelationDefinitionType.ONE_TO_MANY
|
||||
) {
|
||||
return MultipleRecordPickerHotkeyScope.MultipleRecordPicker;
|
||||
}
|
||||
|
||||
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
|
||||
}
|
||||
|
||||
if (isFieldSelect(columnDefinition)) {
|
||||
return SelectFieldHotkeyScope.SelectField;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
|
||||
readonly: isFieldReadOnly,
|
||||
buttonIcon: buttonIcon,
|
||||
customEditHotkeyScope: isFieldRelation(fieldDefinition)
|
||||
? { scope: RelationPickerHotkeyScope.RelationPicker }
|
||||
: undefined,
|
||||
IconLabel: fieldDefinition.iconName
|
||||
? getIcon(fieldDefinition.iconName)
|
||||
: undefined,
|
||||
@ -135,8 +170,10 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
isDisplayModeFixHeight: isDisplayModeFixHeight,
|
||||
editModeContentOnly: isFieldInputOnly,
|
||||
loading: loading,
|
||||
onOpenEditMode,
|
||||
onCloseEditMode,
|
||||
customEditHotkeyScope: computedHotkeyScope(fieldDefinition),
|
||||
onOpenEditMode:
|
||||
onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })),
|
||||
onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { createContext, ReactElement, useContext } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
@ -12,7 +11,7 @@ export type RecordInlineCellContextProps = {
|
||||
editModeContent?: ReactElement;
|
||||
editModeContentOnly?: boolean;
|
||||
displayModeContent?: ReactElement;
|
||||
customEditHotkeyScope?: HotkeyScope;
|
||||
customEditHotkeyScope?: string;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
loading?: boolean;
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
|
||||
import {
|
||||
MiddlewareState,
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
@ -24,6 +35,33 @@ export const RecordInlineCellEditMode = ({
|
||||
children,
|
||||
}: RecordInlineCellEditModeProps) => {
|
||||
const { isCentered } = useContext(RecordInlineCellContext);
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const instanceId = getRecordFieldInputId(
|
||||
recordId,
|
||||
fieldDefinition?.metadata?.fieldName,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirection = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const setFieldInputLayoutDirectionMiddleware = {
|
||||
name: 'middleware',
|
||||
fn: async (state: MiddlewareState) => {
|
||||
setFieldInputLayoutDirection(
|
||||
state.placement.startsWith('bottom') ? 'downward' : 'upward',
|
||||
);
|
||||
setFieldInputLayoutDirectionLoading(false);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: isCentered ? 'bottom' : 'bottom-start',
|
||||
@ -40,6 +78,7 @@ export const RecordInlineCellEditMode = ({
|
||||
crossAxis: -5,
|
||||
},
|
||||
),
|
||||
setFieldInputLayoutDirectionMiddleware,
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
||||
@ -48,16 +47,13 @@ export const useInlineCell = () => {
|
||||
goBackToPreviousDropdownFocusId();
|
||||
};
|
||||
|
||||
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
||||
const openInlineCell = (customEditHotkeyScopeForField?: string) => {
|
||||
onOpenEditMode?.();
|
||||
setIsInlineCellInEditMode(true);
|
||||
initFieldInputDraftValue({ recordId, fieldDefinition });
|
||||
|
||||
if (isDefined(customEditHotkeyScopeForField)) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
customEditHotkeyScopeForField.scope,
|
||||
customEditHotkeyScopeForField.customScopes,
|
||||
);
|
||||
setHotkeyScopeAndMemorizePreviousScope(customEditHotkeyScopeForField);
|
||||
} else {
|
||||
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
|
||||
}
|
||||
|
||||
@ -1,204 +0,0 @@
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/components/MultipleRecordPickerMenuItem';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerProps = {
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
onSubmit?: () => void;
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
dropdownPlacement?: Placement | null;
|
||||
componentInstanceId: string;
|
||||
};
|
||||
|
||||
export const MultipleRecordPicker = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCreate,
|
||||
dropdownPlacement,
|
||||
componentInstanceId,
|
||||
}: MultipleRecordPickerProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
|
||||
useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
const recordMultiSelectIsLoading = useRecoilValue(
|
||||
recordMultiSelectIsLoadingState,
|
||||
);
|
||||
|
||||
const objectRecordsIdsMultiSelect = useRecoilValue(
|
||||
objectRecordsIdsMultiSelectState,
|
||||
);
|
||||
|
||||
const setSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.();
|
||||
goBackToPreviousHotkeyScope();
|
||||
resetSelectedItem();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleSubmit();
|
||||
},
|
||||
instanceId,
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: handleSubmit,
|
||||
listenerId: 'MULTI_RECORD_SELECT_LISTENER_ID',
|
||||
hotkeyScope: instanceId,
|
||||
});
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchFilter(event.currentTarget.value);
|
||||
},
|
||||
[setSearchFilter],
|
||||
);
|
||||
|
||||
// TODO: refactor this in a separate component
|
||||
const results = (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
selectableListId={RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID}
|
||||
selectableItemIdArray={objectRecordsIdsMultiSelect}
|
||||
hotkeyScope={instanceId}
|
||||
onEnter={(selectedId) => {
|
||||
onChange?.(selectedId);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
>
|
||||
{objectRecordsIdsMultiSelect?.map((recordId) => {
|
||||
return (
|
||||
<MultipleRecordPickerMenuItem
|
||||
key={recordId}
|
||||
objectRecordId={recordId}
|
||||
onChange={(recordId) => {
|
||||
onChange?.(recordId);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
|
||||
const createNewButton = isDefined(onCreate) && (
|
||||
<CreateNewButton
|
||||
onClick={() => onCreate?.(recordPickerSearchFilter)}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && results}
|
||||
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSearchInput
|
||||
value={recordPickerSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
{(dropdownPlacement?.includes('start') ||
|
||||
isUndefinedOrNull(dropdownPlacement)) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{recordMultiSelectIsLoading && !recordPickerSearchFilter && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && results}
|
||||
{objectRecordsIdsMultiSelect?.length > 0 && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,88 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MultipleRecordPickerMenuItem = ({
|
||||
objectRecordId,
|
||||
onChange,
|
||||
}: {
|
||||
objectRecordId: string;
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
}) => {
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const isSelectedByKeyboard = useRecoilValue(
|
||||
isSelectedItemIdSelector(objectRecordId),
|
||||
);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const {
|
||||
objectRecordMultiSelectFamilyState,
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
} = useObjectRecordMultiSelectScopedStates(instanceId);
|
||||
|
||||
const record = useRecoilValue(
|
||||
objectRecordMultiSelectFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
const objectRecordMultiSelectCheckedRecordsIds = useRecoilValue(
|
||||
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||
);
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectChange = () => {
|
||||
onChange?.(objectRecordId);
|
||||
};
|
||||
|
||||
const { recordIdentifier } = record;
|
||||
|
||||
if (!isDefined(recordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selected = objectRecordMultiSelectCheckedRecordsIds.find(
|
||||
(checkedObjectRecord) => checkedObjectRecord === objectRecordId,
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={objectRecordId} key={objectRecordId}>
|
||||
<MenuItemMultiSelectAvatar
|
||||
onSelectChange={(_isNewlySelectedValue) => handleSelectChange()}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={selected}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholderColorSeed={objectRecordId}
|
||||
placeholder={recordIdentifier.name}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={recordIdentifier.name}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export const RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'record-picker-click-outside-listener';
|
||||
@ -1,2 +0,0 @@
|
||||
export const RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID =
|
||||
'record-picker-selectable-list-component-instance-id';
|
||||
@ -0,0 +1,38 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = {
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const useRecordPickerGetRecordAndObjectMetadataItemFromRecordId = ({
|
||||
recordId,
|
||||
}: UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const pickableMorphItem = useRecoilComponentFamilyValueV2(
|
||||
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
|
||||
recordId,
|
||||
);
|
||||
|
||||
const record = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
if (!isDefined(pickableMorphItem)) {
|
||||
return { record: null, objectMetadataItem: null };
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === pickableMorphItem.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return { record: null, objectMetadataItem: null };
|
||||
}
|
||||
|
||||
return { record, objectMetadataItem };
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const useRecordPickerRecordsOptions = ({
|
||||
objectNameSingular,
|
||||
selectedRecordIds = [],
|
||||
excludedRecordIds = [],
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
selectedRecordIds?: string[];
|
||||
excludedRecordIds?: string[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const records = useFilteredSearchRecordQuery({
|
||||
searchFilter: recordPickerSearchFilter,
|
||||
selectedIds: selectedRecordIds,
|
||||
excludedRecordIds: excludedRecordIds,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return { records };
|
||||
};
|
||||
@ -0,0 +1,170 @@
|
||||
import { MultipleRecordPickerMenuItems } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems';
|
||||
import { MultipleRecordPickerOnClickOutsideEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect';
|
||||
import { MultipleRecordPickerSearchInput } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerIsLoadingComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { multipleRecordPickerPickableMorphItemsLengthComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerProps = {
|
||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
onSubmit?: () => void;
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
layoutDirection?: RecordPickerLayoutDirection;
|
||||
componentInstanceId: string;
|
||||
onClickOutside: () => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPicker = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCreate,
|
||||
onClickOutside,
|
||||
layoutDirection = 'search-bar-on-bottom',
|
||||
componentInstanceId,
|
||||
}: MultipleRecordPickerProps) => {
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerIsLoading = useRecoilComponentValueV2(
|
||||
multipleRecordPickerIsLoadingComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const itemsLength = useRecoilComponentValueV2(
|
||||
multipleRecordPickerPickableMorphItemsLengthComponentSelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerSearchFilterState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerSearchFilterComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.();
|
||||
goBackToPreviousHotkeyScope();
|
||||
resetSelectedItem();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleSubmit();
|
||||
},
|
||||
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleCreateNewButtonClick = useRecoilCallback(
|
||||
({ snapshot }) => {
|
||||
return () => {
|
||||
const recordPickerSearchFilter = snapshot
|
||||
.getLoadable(multipleRecordPickerSearchFilterState)
|
||||
.getValue();
|
||||
onCreate?.(recordPickerSearchFilter);
|
||||
};
|
||||
},
|
||||
[multipleRecordPickerSearchFilterState, onCreate],
|
||||
);
|
||||
|
||||
const createNewButton = isDefined(onCreate) && (
|
||||
<CreateNewButton
|
||||
onClick={handleCreateNewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add New"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MultipleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<MultipleRecordPickerOnClickOutsideEffect
|
||||
containerRef={containerRef}
|
||||
onClickOutside={onClickOutside}
|
||||
/>
|
||||
<DropdownMenu ref={containerRef} data-select-disable width={200}>
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{itemsLength > 0 && (
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} />
|
||||
)}
|
||||
{multipleRecordPickerIsLoading && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{itemsLength > 0 && <DropdownMenuSeparator />}
|
||||
</>
|
||||
)}
|
||||
<MultipleRecordPickerSearchInput />
|
||||
{layoutDirection === 'search-bar-on-top' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{multipleRecordPickerIsLoading && (
|
||||
<>
|
||||
<DropdownMenuSkeletonItem />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{itemsLength > 0 && (
|
||||
<MultipleRecordPickerMenuItems onChange={onChange} />
|
||||
)}
|
||||
{itemsLength > 0 && <DropdownMenuSeparator />}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</MultipleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRecordPickerGetRecordAndObjectMetadataItemFromRecordId } from '@/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId';
|
||||
import { MultipleRecordPickerMenuItemContent } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemProps = {
|
||||
recordId: string;
|
||||
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItem = ({
|
||||
recordId,
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemProps) => {
|
||||
const { record, objectMetadataItem } =
|
||||
useRecordPickerGetRecordAndObjectMetadataItemFromRecordId({
|
||||
recordId,
|
||||
});
|
||||
|
||||
if (!isDefined(record) || !isDefined(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MultipleRecordPickerMenuItemContent
|
||||
record={record}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,92 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemMultiSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerIsSelectedComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemContentProps = {
|
||||
record: ObjectRecord;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
onChange: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItemContent = ({
|
||||
record,
|
||||
objectMetadataItem,
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemContentProps) => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedByKeyboard = useRecoilValue(
|
||||
isSelectedItemIdSelector(record.id),
|
||||
);
|
||||
|
||||
const isRecordSelectedWithObjectItem = useRecoilComponentFamilyValueV2(
|
||||
multipleRecordPickerIsSelectedComponentFamilySelector,
|
||||
record.id,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const handleSelectChange = (isSelected: boolean) => {
|
||||
onChange({
|
||||
recordId: record.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
isSelected,
|
||||
isMatchingSearchFilter: true,
|
||||
});
|
||||
};
|
||||
|
||||
const recordIdentifier = getObjectRecordIdentifier({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
|
||||
if (!isDefined(recordIdentifier)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||
<MenuItemMultiSelectAvatar
|
||||
onSelectChange={(isSelected) => handleSelectChange(isSelected)}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={isRecordSelectedWithObjectItem}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholderColorSeed={record.id}
|
||||
placeholder={recordIdentifier.name}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={recordIdentifier.name}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,138 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MultipleRecordPickerMenuItem } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector';
|
||||
import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector';
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type MultipleRecordPickerMenuItemsProps = {
|
||||
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
|
||||
};
|
||||
|
||||
export const MultipleRecordPickerMenuItems = ({
|
||||
onChange,
|
||||
}: MultipleRecordPickerMenuItemsProps) => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getMultipleRecordPickerSelectableListId(componentInstanceId);
|
||||
|
||||
const pickableRecordIds = useRecoilComponentValueV2(
|
||||
multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const { resetSelectedItem } = useSelectableList(
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
const singlePickableMorphItemFamilySelector =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerSinglePickableMorphItemComponentFamilySelector,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerPickableMorphItemsState =
|
||||
useRecoilComponentCallbackStateV2(
|
||||
multipleRecordPickerPickableMorphItemsComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return (morphItem: RecordPickerPickableMorphItem) => {
|
||||
const previousMorphItems = snapshot
|
||||
.getLoadable(multipleRecordPickerPickableMorphItemsState)
|
||||
.getValue();
|
||||
|
||||
const existingMorphItemIndex = previousMorphItems.findIndex(
|
||||
(item) => item.recordId === morphItem.recordId,
|
||||
);
|
||||
|
||||
const newMorphItems = [...previousMorphItems];
|
||||
|
||||
if (existingMorphItemIndex === -1) {
|
||||
newMorphItems.push(morphItem);
|
||||
} else {
|
||||
newMorphItems[existingMorphItemIndex] = morphItem;
|
||||
}
|
||||
|
||||
set(multipleRecordPickerPickableMorphItemsState, newMorphItems);
|
||||
};
|
||||
},
|
||||
[multipleRecordPickerPickableMorphItemsState],
|
||||
);
|
||||
|
||||
const handleEnter = useRecoilCallback(
|
||||
({ snapshot }) => {
|
||||
return (selectedId: string) => {
|
||||
const pickableMorphItem = snapshot
|
||||
.getLoadable(singlePickableMorphItemFamilySelector(selectedId))
|
||||
.getValue();
|
||||
|
||||
if (!isDefined(pickableMorphItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedMorphItem = {
|
||||
...pickableMorphItem,
|
||||
isSelected: !pickableMorphItem.isSelected,
|
||||
};
|
||||
|
||||
handleChange(selectedMorphItem);
|
||||
onChange?.(selectedMorphItem);
|
||||
resetSelectedItem();
|
||||
};
|
||||
},
|
||||
[
|
||||
handleChange,
|
||||
onChange,
|
||||
resetSelectedItem,
|
||||
singlePickableMorphItemFamilySelector,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
selectableListId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={pickableRecordIds}
|
||||
hotkeyScope={MultipleRecordPickerHotkeyScope.MultipleRecordPicker}
|
||||
onEnter={handleEnter}
|
||||
>
|
||||
{pickableRecordIds.map((recordId) => {
|
||||
return (
|
||||
<MultipleRecordPickerMenuItem
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
onChange={(morphItem) => {
|
||||
handleChange(morphItem);
|
||||
onChange?.(morphItem);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-picker/multiple-record-picker/constants/MultipleRecordPickerClickOutsideListenerId';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
export const MultipleRecordPickerOnClickOutsideEffect = ({
|
||||
containerRef,
|
||||
onClickOutside,
|
||||
}: {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
onClickOutside: () => void;
|
||||
}) => {
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClickOutside();
|
||||
},
|
||||
listenerId: MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MultipleRecordPickerSearchInput = () => {
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
MultipleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const [recordPickerSearchFilter, setRecordPickerSearchFilter] =
|
||||
useRecoilComponentStateV2(multipleRecordPickerSearchFilterComponentState);
|
||||
|
||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRecordPickerSearchFilter(event.currentTarget.value);
|
||||
performSearch({
|
||||
multipleRecordPickerInstanceId: componentInstanceId,
|
||||
forceSearchFilter: event.currentTarget.value,
|
||||
});
|
||||
},
|
||||
[componentInstanceId, performSearch, setRecordPickerSearchFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuSearchInput
|
||||
value={recordPickerSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export const MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'multiple-record-picker-click-outside-listener';
|
||||
@ -0,0 +1,339 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||
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 { multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId } from '@/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ApolloClient, useApolloClient } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared';
|
||||
|
||||
export const useMultipleRecordPickerPerformSearch = () => {
|
||||
const client = useApolloClient();
|
||||
|
||||
const performSearch = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async ({
|
||||
multipleRecordPickerInstanceId,
|
||||
forceSearchFilter = '',
|
||||
forceSearchableObjectMetadataItems = [],
|
||||
forcePickableMorphItems = [],
|
||||
}: {
|
||||
multipleRecordPickerInstanceId: string;
|
||||
forceSearchFilter?: string;
|
||||
forceSearchableObjectMetadataItems?: ObjectMetadataItem[];
|
||||
forcePickableMorphItems?: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerSearchFilterComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const searchFilter = forceSearchFilter ?? recordPickerSearchFilter;
|
||||
|
||||
const recordPickerSearchableObjectMetadataItems = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
|
||||
{ instanceId: multipleRecordPickerInstanceId },
|
||||
),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const searchableObjectMetadataItems =
|
||||
forceSearchableObjectMetadataItems.length > 0
|
||||
? forceSearchableObjectMetadataItems
|
||||
: recordPickerSearchableObjectMetadataItems;
|
||||
|
||||
const recordPickerPickableMorphItems = snapshot
|
||||
.getLoadable(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const pickableMorphItems =
|
||||
forcePickableMorphItems.length > 0
|
||||
? forcePickableMorphItems
|
||||
: recordPickerPickableMorphItems;
|
||||
|
||||
const recordsWithObjectMetadataIdFilteredOnPickedRecords =
|
||||
await performSearchForPickedRecords({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
});
|
||||
|
||||
const recordsWithObjectMetadataIdExcludingPickedRecords =
|
||||
await performSearchExcludingPickedRecords({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
});
|
||||
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
// We update the existing pickedMorphItems to be matching the search filter
|
||||
const updatedPickedMorphItems = pickedMorphItems.map((morphItem) => {
|
||||
const record =
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.find(
|
||||
({ record }) => record.id === morphItem.recordId,
|
||||
);
|
||||
|
||||
return {
|
||||
...morphItem,
|
||||
isMatchingSearchFilter: isDefined(record),
|
||||
};
|
||||
});
|
||||
|
||||
const recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates =
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords.filter(
|
||||
({ record }) =>
|
||||
!updatedPickedMorphItems.some(
|
||||
({ recordId }) => recordId === record.id,
|
||||
),
|
||||
);
|
||||
|
||||
const recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates =
|
||||
recordsWithObjectMetadataIdExcludingPickedRecords.filter(
|
||||
({ record }) =>
|
||||
!recordsWithObjectMetadataIdFilteredOnPickedRecords.some(
|
||||
({ record: recordFilteredOnPickedRecords }) =>
|
||||
recordFilteredOnPickedRecords.id === record.id,
|
||||
) &&
|
||||
!pickedMorphItems.some(({ recordId }) => recordId === record.id),
|
||||
);
|
||||
|
||||
const morphItems = [
|
||||
...updatedPickedMorphItems,
|
||||
...recordsWithObjectMetadataIdFilteredOnPickedRecordsWithoutDuplicates.map(
|
||||
({ record, objectMetadataItem }) => ({
|
||||
isMatchingSearchFilter: true,
|
||||
isSelected: true,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
}),
|
||||
),
|
||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates.map(
|
||||
({ record, objectMetadataItem }) => ({
|
||||
isMatchingSearchFilter: true,
|
||||
isSelected: false,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
recordId: record.id,
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
set(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId: multipleRecordPickerInstanceId,
|
||||
}),
|
||||
morphItems,
|
||||
);
|
||||
|
||||
[
|
||||
...recordsWithObjectMetadataIdFilteredOnPickedRecords,
|
||||
...recordsWithObjectMetadataIdExcludingPickedRecordsWithoutDuplicates,
|
||||
].forEach(({ record }) => {
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
});
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
return { performSearch };
|
||||
};
|
||||
|
||||
const performSearchForPickedRecords = async ({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
}: {
|
||||
client: ApolloClient<object>;
|
||||
searchFilter: string;
|
||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
const filterPerMetadataItemFilteredOnPickedRecordId = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ id, nameSingular }) => {
|
||||
const pickedRecordIdsForMetadataItem = pickedMorphItems
|
||||
.filter(
|
||||
({ objectMetadataId, isSelected }) =>
|
||||
objectMetadataId === id && isSelected,
|
||||
)
|
||||
.map(({ recordId }) => recordId);
|
||||
|
||||
if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
id: {
|
||||
in: pickedRecordIdsForMetadataItem,
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const searchableObjectMetadataItemsFilteredOnPickedRecordId =
|
||||
searchableObjectMetadataItems.filter(({ nameSingular }) =>
|
||||
isDefined(
|
||||
filterPerMetadataItemFilteredOnPickedRecordId[
|
||||
`filter${capitalize(nameSingular)}`
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!isNonEmptyArray(searchableObjectMetadataItemsFilteredOnPickedRecordId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const combinedSearchRecordsQueryFilteredOnPickedRecords =
|
||||
generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems:
|
||||
searchableObjectMetadataItemsFilteredOnPickedRecordId,
|
||||
operationSignatures:
|
||||
searchableObjectMetadataItemsFilteredOnPickedRecordId.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const limitPerMetadataItem = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ nameSingular }) => {
|
||||
return [`limit${capitalize(nameSingular)}`, 10];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { data: combinedSearchRecordFilteredOnPickedRecordsQueryResult } =
|
||||
await client.query<CombinedFindManyRecordsQueryResult>({
|
||||
query: combinedSearchRecordsQueryFilteredOnPickedRecords,
|
||||
variables: {
|
||||
search: searchFilter,
|
||||
...limitPerMetadataItem,
|
||||
...filterPerMetadataItemFilteredOnPickedRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
recordsWithObjectMetadataId:
|
||||
recordsWithObjectMetadataIdFilteredOnPickedRecords,
|
||||
} = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
searchQueryResult: combinedSearchRecordFilteredOnPickedRecordsQueryResult,
|
||||
});
|
||||
|
||||
return recordsWithObjectMetadataIdFilteredOnPickedRecords;
|
||||
};
|
||||
|
||||
const performSearchExcludingPickedRecords = async ({
|
||||
client,
|
||||
searchFilter,
|
||||
searchableObjectMetadataItems,
|
||||
pickableMorphItems,
|
||||
}: {
|
||||
client: ApolloClient<object>;
|
||||
searchFilter: string;
|
||||
searchableObjectMetadataItems: ObjectMetadataItem[];
|
||||
pickableMorphItems: RecordPickerPickableMorphItem[];
|
||||
}) => {
|
||||
if (searchableObjectMetadataItems.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pickedMorphItems = pickableMorphItems.filter(
|
||||
({ isSelected }) => isSelected,
|
||||
);
|
||||
|
||||
const filterPerMetadataItemExcludingPickedRecordId = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ id, nameSingular }) => {
|
||||
const pickedRecordIdsForMetadataItem = pickedMorphItems
|
||||
.filter(
|
||||
({ objectMetadataId, isSelected }) =>
|
||||
objectMetadataId === id && isSelected,
|
||||
)
|
||||
.map(({ recordId }) => recordId);
|
||||
|
||||
if (!isNonEmptyArray(pickedRecordIdsForMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
not: {
|
||||
id: {
|
||||
in: pickedRecordIdsForMetadataItem,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const combinedSearchRecordsQueryExcludingPickedRecords =
|
||||
generateCombinedSearchRecordsQuery({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
operationSignatures: searchableObjectMetadataItems.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
variables: {},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const limitPerMetadataItem = Object.fromEntries(
|
||||
searchableObjectMetadataItems
|
||||
.map(({ nameSingular }) => {
|
||||
return [`limit${capitalize(nameSingular)}`, 10];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { data: combinedSearchRecordExcludingPickedRecordsQueryResult } =
|
||||
await client.query<CombinedFindManyRecordsQueryResult>({
|
||||
query: combinedSearchRecordsQueryExcludingPickedRecords,
|
||||
variables: {
|
||||
search: searchFilter,
|
||||
...limitPerMetadataItem,
|
||||
...filterPerMetadataItemExcludingPickedRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
recordsWithObjectMetadataId:
|
||||
recordsWithObjectMetadataIdExcludingPickedRecords,
|
||||
} = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({
|
||||
objectMetadataItems: searchableObjectMetadataItems,
|
||||
searchQueryResult: combinedSearchRecordExcludingPickedRecordsQueryResult,
|
||||
});
|
||||
|
||||
return recordsWithObjectMetadataIdExcludingPickedRecords;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { ComponentStateKeyV2 } from '@/ui/utilities/state/component-state/types/ComponentStateKeyV2';
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
type MultipleRecordPickerComponentInstanceContextProps = ComponentStateKeyV2;
|
||||
|
||||
export const MultipleRecordPickerComponentInstanceContext =
|
||||
createComponentInstanceContext<MultipleRecordPickerComponentInstanceContextProps>();
|
||||
@ -0,0 +1,9 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerIsLoadingComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'multipleRecordPickerIsLoadingComponentState',
|
||||
defaultValue: false,
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerPickableMorphItemsComponentState =
|
||||
createComponentStateV2<RecordPickerPickableMorphItem[]>({
|
||||
key: 'multipleRecordPickerPickableMorphItemsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerSearchFilterComponentState =
|
||||
createComponentStateV2<string>({
|
||||
key: 'multipleRecordPickerSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
|
||||
export const multipleRecordPickerSearchableObjectMetadataItemsComponentState =
|
||||
createComponentStateV2<ObjectMetadataItem[]>({
|
||||
key: 'multipleRecordPickerSearchableObjectMetadataItemsComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
|
||||
|
||||
export const multipleRecordPickerIsSelectedComponentFamilySelector =
|
||||
createComponentFamilySelectorV2<boolean, string>({
|
||||
key: 'visibleRecordGroupIdsComponentFamilySelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey: recordId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const pickableMorphItem = pickableMorphItems.find(
|
||||
({ recordId: itemRecordId }) => itemRecordId === recordId,
|
||||
);
|
||||
|
||||
return pickableMorphItem?.isSelected ?? false;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
|
||||
export const multipleRecordPickerPickableMorphItemsLengthComponentSelector =
|
||||
createComponentSelectorV2({
|
||||
key: 'multipleRecordPickerPickableMorphItemsLengthComponentSelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pickableMorphItems.length;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
|
||||
export const multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector =
|
||||
createComponentSelectorV2({
|
||||
key: 'multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pickableMorphItems
|
||||
.filter(({ isMatchingSearchFilter }) => isMatchingSearchFilter)
|
||||
.map(({ recordId }) => recordId);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
|
||||
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
|
||||
import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2';
|
||||
|
||||
export const multipleRecordPickerSinglePickableMorphItemComponentFamilySelector =
|
||||
createComponentFamilySelectorV2<
|
||||
RecordPickerPickableMorphItem | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'multipleRecordPickerSinglePickableMorphItemComponentFamilySelector',
|
||||
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId, familyKey: recordId }) =>
|
||||
({ get }) => {
|
||||
const pickableMorphItems = get(
|
||||
multipleRecordPickerPickableMorphItemsComponentState.atomFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
const pickableMorphItem = pickableMorphItems.find(
|
||||
({ recordId: itemRecordId }) => itemRecordId === recordId,
|
||||
);
|
||||
|
||||
return pickableMorphItem;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
export enum MultipleRecordPickerHotkeyScope {
|
||||
MultipleRecordPicker = 'multiple-record-picker',
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
export type MultipleRecordPickerRecords<
|
||||
CustomRecordForRecordPicker extends SingleRecordPickerRecord,
|
||||
@ -0,0 +1,5 @@
|
||||
export const getMultipleRecordPickerSelectableListId = (
|
||||
multipleRecordPickerComponentInstanceId: string,
|
||||
) => {
|
||||
return `${multipleRecordPickerComponentInstanceId}-selectable-list`;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
|
||||
export const multiRecordPickerFormatSearchResults = (
|
||||
searchResults: CombinedFindManyRecordsQueryResult | undefined | null,
|
||||
): CombinedFindManyRecordsQueryResult => {
|
||||
if (!searchResults) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.entries(searchResults).reduce((acc, [key, value]) => {
|
||||
let newKey = key.replace(/^search/, '');
|
||||
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
||||
acc[newKey] = value;
|
||||
return acc;
|
||||
}, {} as CombinedFindManyRecordsQueryResult);
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult';
|
||||
import { multiRecordPickerFormatSearchResults } from '@/object-record/record-picker/multiple-record-picker/utils/multiRecordPickerFormatSearchResults';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId =
|
||||
({
|
||||
objectMetadataItems,
|
||||
searchQueryResult,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
searchQueryResult: CombinedFindManyRecordsQueryResult;
|
||||
}) => {
|
||||
const formattedSearchQueryResult =
|
||||
multiRecordPickerFormatSearchResults(searchQueryResult);
|
||||
|
||||
const recordsWithObjectMetadataId = Object.entries(
|
||||
formattedSearchQueryResult,
|
||||
).flatMap(([namePlural, objectRecordConnection]) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) return [];
|
||||
|
||||
return objectRecordConnection.edges.map(({ node }) => ({
|
||||
objectMetadataItem,
|
||||
record: node,
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
recordsWithObjectMetadataId,
|
||||
};
|
||||
};
|
||||
@ -3,12 +3,14 @@ import { useRef } from 'react';
|
||||
import {
|
||||
SingleRecordPickerMenuItemsWithSearch,
|
||||
SingleRecordPickerMenuItemsWithSearchProps,
|
||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
export const SINGLE_RECORD_PICKER_LISTENER_ID = 'single-record-select';
|
||||
|
||||
export type SingleRecordPickerProps = {
|
||||
width?: number;
|
||||
componentInstanceId: string;
|
||||
@ -22,9 +24,9 @@ export const SingleRecordPicker = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
width = 200,
|
||||
componentInstanceId,
|
||||
layoutDirection,
|
||||
}: SingleRecordPickerProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -41,11 +43,11 @@ export const SingleRecordPicker = ({
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
listenerId: 'single-record-select',
|
||||
listenerId: SINGLE_RECORD_PICKER_LISTENER_ID,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
<SingleRecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<DropdownMenu ref={containerRef} width={width} data-select-disable>
|
||||
@ -58,10 +60,10 @@ export const SingleRecordPicker = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
layoutDirection,
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -2,10 +2,12 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, MenuItemSelectAvatar } from 'twenty-ui';
|
||||
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
|
||||
type SingleRecordPickerMenuItemProps = {
|
||||
record: SingleRecordPickerRecord;
|
||||
@ -22,11 +24,20 @@ export const SingleRecordPickerMenuItem = ({
|
||||
onRecordSelected,
|
||||
selectedRecord,
|
||||
}: SingleRecordPickerMenuItemProps) => {
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id));
|
||||
|
||||
return (
|
||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||
<MenuItemSelectAvatar
|
||||
@ -1,4 +1,4 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
@ -11,10 +11,15 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/components/SingleRecordPickerMenuItem';
|
||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export type SingleRecordPickerMenuItemsProps = {
|
||||
EmptyIcon?: IconComponent;
|
||||
@ -26,9 +31,12 @@ export type SingleRecordPickerMenuItemsProps = {
|
||||
selectedRecord?: SingleRecordPickerRecord;
|
||||
hotkeyScope?: string;
|
||||
isFiltered: boolean;
|
||||
shouldSelectEmptyOption?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SingleRecordPickerMenuItems = ({
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
@ -37,9 +45,8 @@ export const SingleRecordPickerMenuItems = ({
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
selectedRecord,
|
||||
hotkeyScope = RecordPickerHotkeyScope.RecordPicker,
|
||||
hotkeyScope = SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||
isFiltered,
|
||||
shouldSelectEmptyOption,
|
||||
}: SingleRecordPickerMenuItemsProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -60,8 +67,16 @@ export const SingleRecordPickerMenuItems = ({
|
||||
isDefined(entity) && isNonEmptyString(entity.name),
|
||||
);
|
||||
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const selectableListComponentInstanceId =
|
||||
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||
|
||||
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
|
||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
||||
selectableListComponentInstanceId,
|
||||
);
|
||||
|
||||
const isSelectedSelectNoneButton = useRecoilValue(
|
||||
@ -79,17 +94,21 @@ export const SingleRecordPickerMenuItems = ({
|
||||
);
|
||||
|
||||
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
|
||||
const [selectedRecordId, setSelectedRecordId] = useRecoilComponentStateV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<StyledContainer ref={containerRef}>
|
||||
<SelectableList
|
||||
selectableListId={RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID}
|
||||
selectableListId={selectableListComponentInstanceId}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={hotkeyScope}
|
||||
onEnter={(itemId) => {
|
||||
const recordIndex = recordsInDropdown.findIndex(
|
||||
(record) => record.id === itemId,
|
||||
);
|
||||
setSelectedRecordId(itemId);
|
||||
onRecordSelected(recordsInDropdown[recordIndex]);
|
||||
resetSelectedItem();
|
||||
}}
|
||||
@ -107,10 +126,13 @@ export const SingleRecordPickerMenuItems = ({
|
||||
emptyLabel && (
|
||||
<MenuItemSelect
|
||||
key={record.id}
|
||||
onClick={() => onRecordSelected()}
|
||||
onClick={() => {
|
||||
setSelectedRecordId(undefined);
|
||||
onRecordSelected();
|
||||
}}
|
||||
LeftIcon={EmptyIcon}
|
||||
text={emptyLabel}
|
||||
selected={shouldSelectEmptyOption === true}
|
||||
selected={isUndefined(selectedRecordId)}
|
||||
hovered={isSelectedSelectNoneButton}
|
||||
/>
|
||||
)
|
||||
@ -131,6 +153,6 @@ export const SingleRecordPickerMenuItems = ({
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</SelectableList>
|
||||
</div>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,12 @@
|
||||
import {
|
||||
SingleRecordPickerMenuItems,
|
||||
SingleRecordPickerMenuItemsProps,
|
||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItems';
|
||||
import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions';
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems';
|
||||
import { useSingleRecordPickerRecords } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@ -13,18 +14,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export type SingleRecordPickerMenuItemsWithSearchProps = {
|
||||
excludedRecordIds?: string[];
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
objectNameSingular: string;
|
||||
recordPickerInstanceId?: string;
|
||||
selectedRecordIds: string[];
|
||||
dropdownPlacement?: Placement | null;
|
||||
layoutDirection?: RecordPickerLayoutDirection;
|
||||
} & Pick<
|
||||
SingleRecordPickerMenuItemsProps,
|
||||
| 'EmptyIcon'
|
||||
@ -42,25 +40,23 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
dropdownPlacement,
|
||||
layoutDirection = 'search-bar-on-top',
|
||||
}: SingleRecordPickerMenuItemsWithSearchProps) => {
|
||||
const { handleSearchFilterChange } = useRecordSelectSearch();
|
||||
const { handleSearchFilterChange } = useSingleRecordPickerSearch();
|
||||
|
||||
const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
recordPickerInstanceId,
|
||||
);
|
||||
|
||||
const { records } = useRecordPickerRecordsOptions({
|
||||
const { records } = useSingleRecordPickerRecords({
|
||||
objectNameSingular,
|
||||
selectedRecordIds,
|
||||
excludedRecordIds,
|
||||
});
|
||||
|
||||
@ -72,12 +68,9 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const shouldDisplayDropdownMenuItems =
|
||||
records.recordsToSelect.length + records.selectedRecords?.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
@ -85,22 +78,18 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
{shouldDisplayDropdownMenuItems && (
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||
hotkeyScope={recordPickerInstanceId}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
@ -109,26 +98,21 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
autoFocus
|
||||
role="combobox"
|
||||
/>
|
||||
{(dropdownPlacement?.includes('start') ||
|
||||
isUndefinedOrNull(dropdownPlacement)) && (
|
||||
{layoutDirection === 'search-bar-on-top' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{shouldDisplayDropdownMenuItems && (
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
||||
hotkeyScope={recordPickerInstanceId}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SingleRecordPickerMenuItems
|
||||
recordsToSelect={records.recordsToSelect}
|
||||
loading={records.loading}
|
||||
selectedRecord={records.selectedRecords?.[0]}
|
||||
isFiltered={!!recordPickerSearchFilter}
|
||||
{...{
|
||||
EmptyIcon,
|
||||
emptyLabel,
|
||||
onCancel,
|
||||
onRecordSelected,
|
||||
}}
|
||||
/>
|
||||
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
@ -10,8 +10,8 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { allMockPersonRecords } from '~/testing/mock-data/people';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '../../types/SingleRecordPickerRecord';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
|
||||
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
|
||||
(person) => ({
|
||||
@ -34,7 +34,6 @@ const meta: Meta<typeof SingleRecordPicker> = {
|
||||
],
|
||||
args: {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
selectedRecordIds: [],
|
||||
componentInstanceId: 'single-record-picker',
|
||||
},
|
||||
argTypes: {
|
||||
@ -0,0 +1,2 @@
|
||||
export const SINGLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID =
|
||||
'single-record-picker-click-outside-listener';
|
||||
@ -2,25 +2,25 @@ import { act, renderHook } from '@testing-library/react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const instanceId = 'instanceId';
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||
);
|
||||
|
||||
describe('useRecordSelectSearch', () => {
|
||||
describe('useSingleRecordPickerRecords', () => {
|
||||
it('should update searchFilter after change event', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const recordSelectSearchHook = useRecordSelectSearch(instanceId);
|
||||
const recordSelectSearchHook = useSingleRecordPickerSearch(instanceId);
|
||||
const internallyStoredFilter = useRecoilComponentValueV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
instanceId,
|
||||
);
|
||||
return { recordSelectSearchHook, internallyStoredFilter };
|
||||
@ -0,0 +1,29 @@
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const useSingleRecordPickerRecords = ({
|
||||
objectNameSingular,
|
||||
excludedRecordIds = [],
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
excludedRecordIds?: string[];
|
||||
}) => {
|
||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const selectedRecordId = useRecoilComponentValueV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
);
|
||||
|
||||
const records = useFilteredSearchRecordQuery({
|
||||
searchFilter: recordPickerSearchFilter,
|
||||
selectedIds: selectedRecordId ? [selectedRecordId] : [],
|
||||
excludedRecordIds: excludedRecordIds,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return { records };
|
||||
};
|
||||
@ -1,26 +1,26 @@
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { recordPickerPreselectedIdComponentState } from '@/object-record/record-picker/states/recordPickerPreselectedIdComponentState';
|
||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const useRecordSelectSearch = (
|
||||
export const useSingleRecordPickerSearch = (
|
||||
recordPickerComponentInstanceIdFromProps?: string,
|
||||
) => {
|
||||
const recordPickerComponentInstanceId =
|
||||
useAvailableComponentInstanceIdOrThrow(
|
||||
RecordPickerComponentInstanceContext,
|
||||
SingleRecordPickerComponentInstanceContext,
|
||||
recordPickerComponentInstanceIdFromProps,
|
||||
);
|
||||
|
||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||
recordPickerSearchFilterComponentState,
|
||||
singleRecordPickerSearchFilterComponentState,
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
|
||||
const setRecordPickerPreselectedId = useSetRecoilComponentStateV2(
|
||||
recordPickerPreselectedIdComponentState,
|
||||
const setRecordPickerSelectedId = useSetRecoilComponentStateV2(
|
||||
singleRecordPickerSelectedIdComponentState,
|
||||
recordPickerComponentInstanceId,
|
||||
);
|
||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||
@ -33,14 +33,14 @@ export const useRecordSelectSearch = (
|
||||
|
||||
const resetSearchFilter = () => {
|
||||
debouncedSetSearchFilter('');
|
||||
setRecordPickerPreselectedId('');
|
||||
setRecordPickerSelectedId(undefined);
|
||||
};
|
||||
|
||||
const handleSearchFilterChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
setRecordPickerPreselectedId('');
|
||||
setRecordPickerSelectedId(undefined);
|
||||
};
|
||||
|
||||
return {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user