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 { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
|
||||||
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
|
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
|
||||||
|
import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell';
|
||||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
import { Task } from '@/activities/types/Task';
|
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 { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||||
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
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';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
type ActivityTargetsInlineCellProps = {
|
type ActivityTargetsInlineCellProps = {
|
||||||
@ -38,6 +39,8 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
const { activityTargetObjectRecords } =
|
const { activityTargetObjectRecords } =
|
||||||
useActivityTargetObjectRecords(activity);
|
useActivityTargetObjectRecords(activity);
|
||||||
|
|
||||||
|
const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activity.id}`;
|
||||||
|
|
||||||
const { closeInlineCell } = useInlineCell();
|
const { closeInlineCell } = useInlineCell();
|
||||||
|
|
||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
@ -64,6 +67,12 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
const { openActivityTargetInlineCellEditMode } =
|
const { openActivityTargetInlineCellEditMode } =
|
||||||
useOpenActivityTargetInlineCellEditMode();
|
useOpenActivityTargetInlineCellEditMode();
|
||||||
|
|
||||||
|
const { updateActivityTargetFromInlineCell } =
|
||||||
|
useUpdateActivityTargetFromInlineCell({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
activityId: activity.id,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
||||||
<FieldFocusContextProvider>
|
<FieldFocusContextProvider>
|
||||||
@ -72,20 +81,27 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
<RecordInlineCellContext.Provider
|
<RecordInlineCellContext.Provider
|
||||||
value={{
|
value={{
|
||||||
buttonIcon: IconPencil,
|
buttonIcon: IconPencil,
|
||||||
customEditHotkeyScope: {
|
customEditHotkeyScope:
|
||||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
ActivityEditorHotkeyScope.ActivityTargets,
|
||||||
},
|
|
||||||
IconLabel: showLabel ? IconArrowUpRight : undefined,
|
IconLabel: showLabel ? IconArrowUpRight : undefined,
|
||||||
showLabel: showLabel,
|
showLabel: showLabel,
|
||||||
readonly: isFieldReadOnly,
|
readonly: isFieldReadOnly,
|
||||||
labelWidth: fieldDefinition?.labelWidth,
|
labelWidth: fieldDefinition?.labelWidth,
|
||||||
editModeContent: (
|
editModeContent: (
|
||||||
<ActivityTargetInlineCellEditMode
|
<MultipleRecordPicker
|
||||||
activity={activity}
|
componentInstanceId={multipleRecordPickerInstanceId}
|
||||||
activityTargetWithTargetRecords={
|
onClickOutside={() => {}}
|
||||||
activityTargetObjectRecords
|
onChange={(morphItem) => {
|
||||||
}
|
updateActivityTargetFromInlineCell({
|
||||||
activityObjectNameSingular={activityObjectNameSingular}
|
recordPickerInstanceId: multipleRecordPickerInstanceId,
|
||||||
|
morphItem,
|
||||||
|
activityTargetWithTargetRecords:
|
||||||
|
activityTargetObjectRecords,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
closeInlineCell();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
label: 'Relations',
|
label: 'Relations',
|
||||||
@ -97,7 +113,8 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
),
|
),
|
||||||
onOpenEditMode: () => {
|
onOpenEditMode: () => {
|
||||||
openActivityTargetInlineCellEditMode({
|
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 = {
|
type OpenActivityTargetInlineCellEditModeProps = {
|
||||||
recordPickerInstanceId: string;
|
recordPickerInstanceId: string;
|
||||||
|
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useOpenActivityTargetInlineCellEditMode = () => {
|
export const useOpenActivityTargetInlineCellEditMode = () => {
|
||||||
const openActivityTargetInlineCellEditMode = ({
|
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
|
||||||
recordPickerInstanceId,
|
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
|
||||||
}: OpenActivityTargetInlineCellEditModeProps) => {
|
|
||||||
// eslint-disable-next-line no-console
|
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||||
console.log('openActivityTargetInlineCellEditMode', recordPickerInstanceId);
|
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 };
|
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 { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
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 instanceId = 'instanceId';
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||||
<RecoilRoot>{children}</RecoilRoot>
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('useLimitPerMetadataItem', () => {
|
describe('useLimitPerMetadataItem', () => {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export const getLabelIdentifierFieldValue = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
|
if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
|
||||||
return String(record[labelIdentifierFieldMetadataItem.name]);
|
return record[labelIdentifierFieldMetadataItem.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@ -12,7 +12,13 @@ export const getObjectRecordIdentifier = ({
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
record,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: Pick<
|
||||||
|
ObjectMetadataItem,
|
||||||
|
| 'fields'
|
||||||
|
| 'labelIdentifierFieldMetadataId'
|
||||||
|
| 'nameSingular'
|
||||||
|
| 'imageIdentifierFieldMetadataId'
|
||||||
|
>;
|
||||||
record: ObjectRecord;
|
record: ObjectRecord;
|
||||||
}): ObjectRecordIdentifier => {
|
}): ObjectRecordIdentifier => {
|
||||||
const labelIdentifierFieldMetadataItem =
|
const labelIdentifierFieldMetadataItem =
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
|||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
||||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
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 = ({
|
export const useCombinedFindManyRecords = ({
|
||||||
operationSignatures,
|
operationSignatures,
|
||||||
@ -22,7 +22,7 @@ export const useCombinedFindManyRecords = ({
|
|||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, loading } = useQuery<MultiObjectRecordQueryResult>(
|
const { data, loading } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||||
findManyQuery ?? EMPTY_QUERY,
|
findManyQuery ?? EMPTY_QUERY,
|
||||||
{
|
{
|
||||||
skip,
|
skip,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
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 = ({
|
export const useCombinedGetTotalCount = ({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
@ -28,7 +28,7 @@ export const useCombinedGetTotalCount = ({
|
|||||||
operationSignatures,
|
operationSignatures,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = useQuery<MultiObjectRecordQueryResult>(
|
const { data } = useQuery<CombinedFindManyRecordsQueryResult>(
|
||||||
findManyQuery ?? EMPTY_QUERY,
|
findManyQuery ?? EMPTY_QUERY,
|
||||||
{
|
{
|
||||||
skip,
|
skip,
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import { isUndefined } from '@sniptt/guards';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
|
||||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery';
|
||||||
import { capitalize } from 'twenty-shared';
|
|
||||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||||
|
|
||||||
export const useGenerateCombinedSearchRecordsQuery = ({
|
export const useGenerateCombinedSearchRecordsQuery = ({
|
||||||
@ -20,70 +16,8 @@ export const useGenerateCombinedSearchRecordsQuery = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterPerMetadataItemArray = operationSignatures
|
return generateCombinedSearchRecordsQuery({
|
||||||
.map(
|
objectMetadataItems,
|
||||||
({ objectNameSingular }) =>
|
operationSignatures,
|
||||||
`$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')}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||||
|
|
||||||
export type MultiObjectRecordQueryResult = {
|
export type CombinedFindManyRecordsQueryResult = {
|
||||||
[namePlural: string]: RecordGqlConnection;
|
[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 { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
|
||||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
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 { 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 { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
@ -91,7 +91,7 @@ export const ObjectFilterDropdownBooleanSelect = () => {
|
|||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId="boolean-select"
|
selectableListId="boolean-select"
|
||||||
selectableItemIdArray={options.map((option) => option.toString())}
|
selectableItemIdArray={options.map((option) => option.toString())}
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||||
onEnter={(itemId) => {
|
onEnter={(itemId) => {
|
||||||
handleOptionSelect(itemId === 'true');
|
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 { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters';
|
||||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
@ -87,7 +87,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({
|
|||||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||||
|
|
||||||
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
if (filterType === 'RELATION' || filterType === 'SELECT') {
|
||||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOperand = getRecordFilterOperands({
|
const defaultOperand = getRecordFilterOperands({
|
||||||
|
|||||||
@ -18,9 +18,8 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o
|
|||||||
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
|
import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState';
|
||||||
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState';
|
||||||
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
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 { 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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
@ -102,7 +101,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
|||||||
closeDropdown();
|
closeDropdown();
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
},
|
},
|
||||||
RelationPickerHotkeyScope.RelationPicker,
|
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||||
[closeDropdown, resetSelectedItem],
|
[closeDropdown, resetSelectedItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -165,7 +164,7 @@ export const ObjectFilterDropdownOptionSelect = () => {
|
|||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId={componentInstanceId}
|
selectableListId={componentInstanceId}
|
||||||
selectableItemIdArray={objectRecordsIds}
|
selectableItemIdArray={objectRecordsIds}
|
||||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||||
onEnter={(itemId) => {
|
onEnter={(itemId) => {
|
||||||
const option = optionsInDropdown.find((option) => option.id === itemId);
|
const option = optionsInDropdown.find((option) => option.id === itemId);
|
||||||
if (isDefined(option)) {
|
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 { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
|
||||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
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 { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||||
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
|
import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect';
|
||||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||||
@ -232,7 +232,7 @@ export const ObjectFilterDropdownRecordSelect = ({
|
|||||||
)}
|
)}
|
||||||
<MultipleSelectDropdown
|
<MultipleSelectDropdown
|
||||||
selectableListId="object-filter-record-select-id"
|
selectableListId="object-filter-record-select-id"
|
||||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||||
itemsToSelect={recordsToSelect}
|
itemsToSelect={recordsToSelect}
|
||||||
filteredSelectedItems={filteredSelectedRecords}
|
filteredSelectedItems={filteredSelectedRecords}
|
||||||
selectedItems={selectedRecords}
|
selectedItems={selectedRecords}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-
|
|||||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||||
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
|
||||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
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 { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown';
|
||||||
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
import { SelectableItem } from '@/object-record/select/types/SelectableItem';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
@ -144,7 +144,7 @@ export const ObjectFilterDropdownSourceSelect = ({
|
|||||||
return (
|
return (
|
||||||
<MultipleSelectDropdown
|
<MultipleSelectDropdown
|
||||||
selectableListId="object-filter-source-select-id"
|
selectableListId="object-filter-source-select-id"
|
||||||
hotkeyScope={RecordPickerHotkeyScope.RecordPicker}
|
hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker}
|
||||||
itemsToSelect={sourceTypes.filter(
|
itemsToSelect={sourceTypes.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
!filteredSelectedItems.some((selected) => selected.id === item.id),
|
!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 { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||||
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter';
|
||||||
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
|
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
@ -67,7 +67,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => {
|
|||||||
fieldMetadataItem.type === 'RELATION' ||
|
fieldMetadataItem.type === 'RELATION' ||
|
||||||
fieldMetadataItem.type === 'SELECT'
|
fieldMetadataItem.type === 'SELECT'
|
||||||
) {
|
) {
|
||||||
setHotkeyScope(RecordPickerHotkeyScope.RecordPicker);
|
setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type);
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
|
||||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||||
@ -68,22 +67,15 @@ export const RecordBoardColumnNewOpportunity = ({
|
|||||||
<>
|
<>
|
||||||
{newRecord.isCreating && newRecord.position === position && (
|
{newRecord.isCreating && newRecord.position === position && (
|
||||||
<OverlayContainer>
|
<OverlayContainer>
|
||||||
<RecordPickerComponentInstanceContext.Provider
|
<SingleRecordPicker
|
||||||
value={{
|
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
||||||
instanceId: `add-new-card-record-picker-column-${columnId}`,
|
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
||||||
}}
|
onRecordSelected={(company) =>
|
||||||
>
|
company ? handleEntitySelect(position, company) : null
|
||||||
<SingleRecordPicker
|
}
|
||||||
componentInstanceId={`add-new-card-record-picker-column-${columnId}`}
|
objectNameSingular={CoreObjectNameSingular.Company}
|
||||||
onCancel={() => handleCreateSuccess(position, columnId, false)}
|
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
||||||
onRecordSelected={(company) =>
|
/>
|
||||||
company ? handleEntitySelect(position, company) : null
|
|
||||||
}
|
|
||||||
objectNameSingular={CoreObjectNameSingular.Company}
|
|
||||||
selectedRecordIds={[]}
|
|
||||||
onCreate={createCompanyOpportunityAndOpenRightDrawer}
|
|
||||||
/>
|
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
|
||||||
</OverlayContainer>
|
</OverlayContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||||
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope';
|
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useCallback, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { RecoilState, useRecoilCallback } from 'recoil';
|
import { RecoilState, useRecoilCallback } from 'recoil';
|
||||||
@ -26,7 +26,7 @@ export const useAddNewCard = ({
|
|||||||
const columnContext = useContext(RecordBoardColumnContext);
|
const columnContext = useContext(RecordBoardColumnContext);
|
||||||
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
|
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
|
||||||
useContext(RecordBoardContext);
|
useContext(RecordBoardContext);
|
||||||
const { resetSearchFilter } = useRecordSelectSearch(
|
const { resetSearchFilter } = useSingleRecordPickerSearch(
|
||||||
recordPickerComponentInstanceId,
|
recordPickerComponentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export const useAddNewCard = ({
|
|||||||
addNewItem(set, columnDefinitionId, position, isOpportunity);
|
addNewItem(set, columnDefinitionId, position, isOpportunity);
|
||||||
if (isOpportunity) {
|
if (isOpportunity) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
RelationPickerHotkeyScope.RelationPicker,
|
SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createRecord(labelIdentifier, labelValue, position, isOpportunity);
|
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';
|
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||||
|
|
||||||
export type NewCard = {
|
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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
|
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 { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
@ -71,108 +72,114 @@ export const FieldInput = ({
|
|||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordFieldInputScope
|
<RecordFieldComponentInstanceContext.Provider
|
||||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
value={{
|
||||||
|
instanceId: recordFieldInputdId,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
<RecordFieldInputScope
|
||||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
>
|
||||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||||
) : isFieldPhones(fieldDefinition) ? (
|
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
<PhonesFieldInput
|
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||||
onCancel={onCancel}
|
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
) : isFieldPhones(fieldDefinition) ? (
|
||||||
/>
|
<PhonesFieldInput
|
||||||
) : isFieldText(fieldDefinition) ? (
|
onCancel={onCancel}
|
||||||
<TextFieldInput
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldText(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<TextFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldEmails(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<EmailsFieldInput
|
onShiftTab={onShiftTab}
|
||||||
onCancel={onCancel}
|
/>
|
||||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
) : isFieldEmails(fieldDefinition) ? (
|
||||||
/>
|
<EmailsFieldInput
|
||||||
) : isFieldFullName(fieldDefinition) ? (
|
onCancel={onCancel}
|
||||||
<FullNameFieldInput
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldFullName(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<FullNameFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldDateTime(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<DateTimeFieldInput
|
onShiftTab={onShiftTab}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldDateTime(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<DateTimeFieldInput
|
||||||
onClear={onSubmit}
|
onEnter={onEnter}
|
||||||
onSubmit={onSubmit}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldDate(fieldDefinition) ? (
|
onClear={onSubmit}
|
||||||
<DateFieldInput
|
onSubmit={onSubmit}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldDate(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<DateFieldInput
|
||||||
onClear={onSubmit}
|
onEnter={onEnter}
|
||||||
onSubmit={onSubmit}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldNumber(fieldDefinition) ? (
|
onClear={onSubmit}
|
||||||
<NumberFieldInput
|
onSubmit={onSubmit}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldNumber(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<NumberFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldLinks(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<LinksFieldInput
|
onShiftTab={onShiftTab}
|
||||||
onCancel={onCancel}
|
/>
|
||||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
) : isFieldLinks(fieldDefinition) ? (
|
||||||
/>
|
<LinksFieldInput
|
||||||
) : isFieldCurrency(fieldDefinition) ? (
|
onCancel={onCancel}
|
||||||
<CurrencyFieldInput
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldCurrency(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<CurrencyFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldBoolean(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
onShiftTab={onShiftTab}
|
||||||
) : isFieldRating(fieldDefinition) ? (
|
/>
|
||||||
<RatingFieldInput onSubmit={onSubmit} />
|
) : isFieldBoolean(fieldDefinition) ? (
|
||||||
) : isFieldSelect(fieldDefinition) ? (
|
<BooleanFieldInput onSubmit={onSubmit} readonly={isReadOnly} />
|
||||||
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
) : isFieldRating(fieldDefinition) ? (
|
||||||
) : isFieldMultiSelect(fieldDefinition) ? (
|
<RatingFieldInput onSubmit={onSubmit} />
|
||||||
<MultiSelectFieldInput onCancel={onCancel} />
|
) : isFieldSelect(fieldDefinition) ? (
|
||||||
) : isFieldAddress(fieldDefinition) ? (
|
<SelectFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
<AddressFieldInput
|
) : isFieldMultiSelect(fieldDefinition) ? (
|
||||||
onEnter={onEnter}
|
<MultiSelectFieldInput onCancel={onCancel} />
|
||||||
onEscape={onEscape}
|
) : isFieldAddress(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<AddressFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldRawJson(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<RawJsonFieldInput
|
onShiftTab={onShiftTab}
|
||||||
onEnter={onEnter}
|
/>
|
||||||
onEscape={onEscape}
|
) : isFieldRawJson(fieldDefinition) ? (
|
||||||
onClickOutside={onClickOutside}
|
<RawJsonFieldInput
|
||||||
onTab={onTab}
|
onEnter={onEnter}
|
||||||
onShiftTab={onShiftTab}
|
onEscape={onEscape}
|
||||||
/>
|
onClickOutside={onClickOutside}
|
||||||
) : isFieldArray(fieldDefinition) ? (
|
onTab={onTab}
|
||||||
<ArrayFieldInput
|
onShiftTab={onShiftTab}
|
||||||
onCancel={onCancel}
|
/>
|
||||||
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
) : isFieldArray(fieldDefinition) ? (
|
||||||
/>
|
<ArrayFieldInput
|
||||||
) : (
|
onCancel={onCancel}
|
||||||
<></>
|
onClickOutside={(event) => onClickOutside?.(() => {}, event)}
|
||||||
)}
|
/>
|
||||||
</RecordFieldInputScope>
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</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 { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
||||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
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 { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||||
@ -156,13 +155,12 @@ export const usePersistField = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (fieldIsRelationToOneObject) {
|
if (fieldIsRelationToOneObject) {
|
||||||
const value = valueToPersist as SingleRecordPickerRecord;
|
|
||||||
updateRecord?.({
|
updateRecord?.({
|
||||||
variables: {
|
variables: {
|
||||||
where: { id: recordId },
|
where: { id: recordId },
|
||||||
updateOneRecordInput: {
|
updateOneRecordInput: {
|
||||||
[getForeignKeyNameFromRelationFieldName(fieldName)]:
|
[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 { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
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 { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||||
|
|
||||||
export const useRelationField = <
|
export const useRelationField = <T extends ObjectRecord | ObjectRecord[]>() => {
|
||||||
T extends SingleRecordPickerRecord | SingleRecordPickerRecord[],
|
|
||||||
>() => {
|
|
||||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||||
const button = useGetButtonIcon();
|
const button = useGetButtonIcon();
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,14 @@ import { useContext } from 'react';
|
|||||||
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
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 { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker';
|
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
type RelationFromManyFieldInputProps = {
|
type RelationFromManyFieldInputProps = {
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
@ -19,10 +19,9 @@ export const RelationFromManyFieldInput = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: RelationFromManyFieldInputProps) => {
|
}: RelationFromManyFieldInputProps) => {
|
||||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||||
const recordPickerInstanceId = `record-picker-${fieldDefinition.fieldMetadataId}`;
|
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||||
const { updateRelation } = useUpdateRelationFromManyFieldInput({
|
|
||||||
scopeId: recordPickerInstanceId,
|
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
onSubmit?.(() => {});
|
onSubmit?.(() => {});
|
||||||
@ -50,19 +49,22 @@ export const RelationFromManyFieldInput = ({
|
|||||||
recordId,
|
recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const layoutDirection = useRecoilComponentValueV2(
|
||||||
|
recordFieldInputLayoutDirectionComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MultipleRecordPicker
|
||||||
<RecordPickerComponentInstanceContext.Provider
|
componentInstanceId={recordPickerInstanceId}
|
||||||
value={{ instanceId: recordPickerInstanceId }}
|
onSubmit={handleSubmit}
|
||||||
>
|
onChange={updateRelation}
|
||||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
onCreate={createNewRecordAndOpenRightDrawer}
|
||||||
<MultipleRecordPicker
|
onClickOutside={handleSubmit}
|
||||||
componentInstanceId={recordPickerInstanceId}
|
layoutDirection={
|
||||||
onSubmit={handleSubmit}
|
layoutDirection === 'downward'
|
||||||
onChange={updateRelation}
|
? 'search-bar-on-top'
|
||||||
onCreate={createNewRecordAndOpenRightDrawer}
|
: 'search-bar-on-bottom'
|
||||||
/>
|
}
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useRelationField } from '../../hooks/useRelationField';
|
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';
|
import { FieldInputEvent } from './DateTimeFieldInput';
|
||||||
|
|
||||||
export type RelationToOneFieldInputProps = {
|
export type RelationToOneFieldInputProps = {
|
||||||
@ -14,22 +21,64 @@ export const RelationToOneFieldInput = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: RelationToOneFieldInputProps) => {
|
}: RelationToOneFieldInputProps) => {
|
||||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
|
||||||
useRelationField<SingleRecordPickerRecord>();
|
|
||||||
|
|
||||||
const persistField = usePersistField();
|
const persistField = usePersistField();
|
||||||
|
|
||||||
const handleSubmit = (newEntity: SingleRecordPickerRecord | null) => {
|
const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldDefinition.metadata.fieldName}`;
|
||||||
onSubmit?.(() => persistField(newEntity?.record ?? null));
|
|
||||||
};
|
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 (
|
return (
|
||||||
<RelationPicker
|
<SingleRecordPicker
|
||||||
fieldDefinition={fieldDefinition}
|
componentInstanceId={recordPickerInstanceId}
|
||||||
selectedRecordId={fieldValue?.id}
|
EmptyIcon={IconForbid}
|
||||||
onSubmit={handleSubmit}
|
emptyLabel={'No ' + fieldDefinition.label}
|
||||||
onCancel={onCancel}
|
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';
|
} from '~/testing/mock-data/users';
|
||||||
|
|
||||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
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 { getCanvasElementForDropdownTesting } from 'twenty-ui';
|
||||||
import {
|
import {
|
||||||
RelationToOneFieldInput,
|
RelationToOneFieldInput,
|
||||||
@ -30,11 +33,21 @@ const RelationWorkspaceSetterEffect = () => {
|
|||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
|
const setRecordFieldInputLayoutDirectionLoading =
|
||||||
|
useSetRecoilComponentStateV2(
|
||||||
|
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||||
|
'relation-to-one-field-input-123-Relation',
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentWorkspace(mockCurrentWorkspace);
|
setCurrentWorkspace(mockCurrentWorkspace);
|
||||||
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
|
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
|
||||||
}, [setCurrentWorkspace, setCurrentWorkspaceMember]);
|
setRecordFieldInputLayoutDirectionLoading(false);
|
||||||
|
}, [
|
||||||
|
setCurrentWorkspace,
|
||||||
|
setCurrentWorkspaceMember,
|
||||||
|
setRecordFieldInputLayoutDirectionLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
@ -74,12 +87,18 @@ const RelationToOneFieldInputWithContext = ({
|
|||||||
}}
|
}}
|
||||||
recordId={recordId}
|
recordId={recordId}
|
||||||
>
|
>
|
||||||
<RecordPickerComponentInstanceContext.Provider
|
<RecordFieldComponentInstanceContext.Provider
|
||||||
value={{ instanceId: 'relation-to-one-field-input' }}
|
value={{
|
||||||
|
instanceId: 'relation-to-one-field-input-123-Relation',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<RelationWorkspaceSetterEffect />
|
<SingleRecordPickerComponentInstanceContext.Provider
|
||||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
value={{ instanceId: 'relation-to-one-field-input-123-Relation' }}
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
>
|
||||||
|
<RelationWorkspaceSetterEffect />
|
||||||
|
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
|
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||||
|
</RecordFieldComponentInstanceContext.Provider>
|
||||||
</FieldContextProvider>
|
</FieldContextProvider>
|
||||||
<div data-testid="data-field-input-click-outside-div" />
|
<div data-testid="data-field-input-click-outside-div" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
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 { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconEye } from 'twenty-ui';
|
import { IconEye } from 'twenty-ui';
|
||||||
import {
|
import {
|
||||||
|
FeatureFlagKey,
|
||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
RelationDefinitionType,
|
RelationDefinitionType,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
@ -45,6 +48,10 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
|
const { openRecordInCommandMenu } = useCommandMenu();
|
||||||
|
const isCommandMenuEnabled = useIsFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
relationObjectMetadataNameSingular === 'workspaceMember' ||
|
relationObjectMetadataNameSingular === 'workspaceMember' ||
|
||||||
@ -110,10 +117,18 @@ export const useAddNewRecordAndOpenRightDrawer = ({
|
|||||||
|
|
||||||
setViewableRecordId(newRecordId);
|
setViewableRecordId(newRecordId);
|
||||||
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
|
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
|
||||||
openRightDrawer(RightDrawerPages.ViewRecord, {
|
|
||||||
title: 'View Record',
|
if (isCommandMenuEnabled) {
|
||||||
Icon: IconEye,
|
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 { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord';
|
||||||
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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 { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
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';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const useUpdateRelationFromManyFieldInput = ({
|
export const useUpdateRelationFromManyFieldInput = () => {
|
||||||
scopeId,
|
|
||||||
}: {
|
|
||||||
scopeId: string;
|
|
||||||
}) => {
|
|
||||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
assertFieldMetadata(
|
assertFieldMetadata(
|
||||||
@ -41,49 +37,21 @@ export const useUpdateRelationFromManyFieldInput = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateRelation = useRecoilCallback(
|
const updateRelation = useRecoilCallback(
|
||||||
({ snapshot, set }) =>
|
() => async (morphItem: RecordPickerPickableMorphItem) => {
|
||||||
async (objectRecordId: string) => {
|
if (morphItem.isSelected) {
|
||||||
const previouslyCheckedRecordsIds = snapshot
|
await updateOneRecordAndAttachRelations({
|
||||||
.getLoadable(
|
recordId,
|
||||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
relatedRecordId: morphItem.recordId,
|
||||||
scopeId,
|
});
|
||||||
}),
|
} else {
|
||||||
)
|
await updateOneRecordAndDetachRelations({
|
||||||
.getValue();
|
recordId,
|
||||||
|
relatedRecordId: morphItem.recordId,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
[
|
||||||
recordId,
|
recordId,
|
||||||
scopeId,
|
|
||||||
updateOneRecordAndAttachRelations,
|
updateOneRecordAndAttachRelations,
|
||||||
updateOneRecordAndDetachRelations,
|
updateOneRecordAndDetachRelations,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export enum RelationPickerHotkeyScope {
|
export enum RelationPickerHotkeyScope {
|
||||||
RelationPicker = 'relation-picker',
|
|
||||||
AddNew = 'add-new',
|
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 { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||||
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
|
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 { ConnectedAccountProvider } from 'twenty-shared';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
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 FieldSelectValue = string | null;
|
||||||
export type FieldMultiSelectValue = 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<
|
export type FieldRelationValue<
|
||||||
T extends FieldRelationToOneValue | FieldRelationFromManyValue,
|
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 { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
import { FieldDefinition } from '../FieldDefinition';
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
import { FieldMetadata } from '../FieldMetadata';
|
import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
export const isFieldRelationFromManyObjects = (
|
export const isFieldRelationFromManyObjects = (
|
||||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||||
): field is FieldDefinition<FieldMetadata> =>
|
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||||
isFieldRelation(field) &&
|
isFieldRelation(field) &&
|
||||||
field.metadata.relationType === RelationDefinitionType.ONE_TO_MANY;
|
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 { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
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 { useInlineCell } from '../hooks/useInlineCell';
|
||||||
|
|
||||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
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 { 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 { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
|
||||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||||
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||||
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
||||||
import {
|
import {
|
||||||
RecordInlineCellContext,
|
RecordInlineCellContext,
|
||||||
@ -39,6 +46,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
|||||||
onOpenEditMode,
|
onOpenEditMode,
|
||||||
onCloseEditMode,
|
onCloseEditMode,
|
||||||
} = useContext(FieldContext);
|
} = useContext(FieldContext);
|
||||||
|
|
||||||
const buttonIcon = useGetButtonIcon();
|
const buttonIcon = useGetButtonIcon();
|
||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
const isFieldInputOnly = useIsFieldInputOnly();
|
||||||
@ -101,13 +109,40 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
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 = {
|
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
|
||||||
readonly: isFieldReadOnly,
|
readonly: isFieldReadOnly,
|
||||||
buttonIcon: buttonIcon,
|
buttonIcon: buttonIcon,
|
||||||
customEditHotkeyScope: isFieldRelation(fieldDefinition)
|
|
||||||
? { scope: RelationPickerHotkeyScope.RelationPicker }
|
|
||||||
: undefined,
|
|
||||||
IconLabel: fieldDefinition.iconName
|
IconLabel: fieldDefinition.iconName
|
||||||
? getIcon(fieldDefinition.iconName)
|
? getIcon(fieldDefinition.iconName)
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -135,8 +170,10 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
|||||||
isDisplayModeFixHeight: isDisplayModeFixHeight,
|
isDisplayModeFixHeight: isDisplayModeFixHeight,
|
||||||
editModeContentOnly: isFieldInputOnly,
|
editModeContentOnly: isFieldInputOnly,
|
||||||
loading: loading,
|
loading: loading,
|
||||||
onOpenEditMode,
|
customEditHotkeyScope: computedHotkeyScope(fieldDefinition),
|
||||||
onCloseEditMode,
|
onOpenEditMode:
|
||||||
|
onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })),
|
||||||
|
onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|
||||||
import { createContext, ReactElement, useContext } from 'react';
|
import { createContext, ReactElement, useContext } from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
@ -12,7 +11,7 @@ export type RecordInlineCellContextProps = {
|
|||||||
editModeContent?: ReactElement;
|
editModeContent?: ReactElement;
|
||||||
editModeContentOnly?: boolean;
|
editModeContentOnly?: boolean;
|
||||||
displayModeContent?: ReactElement;
|
displayModeContent?: ReactElement;
|
||||||
customEditHotkeyScope?: HotkeyScope;
|
customEditHotkeyScope?: string;
|
||||||
isDisplayModeFixHeight?: boolean;
|
isDisplayModeFixHeight?: boolean;
|
||||||
disableHoverEffect?: boolean;
|
disableHoverEffect?: boolean;
|
||||||
loading?: 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 { 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 { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import styled from '@emotion/styled';
|
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 { useContext } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
@ -24,6 +35,33 @@ export const RecordInlineCellEditMode = ({
|
|||||||
children,
|
children,
|
||||||
}: RecordInlineCellEditModeProps) => {
|
}: RecordInlineCellEditModeProps) => {
|
||||||
const { isCentered } = useContext(RecordInlineCellContext);
|
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({
|
const { refs, floatingStyles } = useFloating({
|
||||||
placement: isCentered ? 'bottom' : 'bottom-start',
|
placement: isCentered ? 'bottom' : 'bottom-start',
|
||||||
@ -40,6 +78,7 @@ export const RecordInlineCellEditMode = ({
|
|||||||
crossAxis: -5,
|
crossAxis: -5,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
setFieldInputLayoutDirectionMiddleware,
|
||||||
],
|
],
|
||||||
whileElementsMounted: autoUpdate,
|
whileElementsMounted: autoUpdate,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2';
|
||||||
@ -48,16 +47,13 @@ export const useInlineCell = () => {
|
|||||||
goBackToPreviousDropdownFocusId();
|
goBackToPreviousDropdownFocusId();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
const openInlineCell = (customEditHotkeyScopeForField?: string) => {
|
||||||
onOpenEditMode?.();
|
onOpenEditMode?.();
|
||||||
setIsInlineCellInEditMode(true);
|
setIsInlineCellInEditMode(true);
|
||||||
initFieldInputDraftValue({ recordId, fieldDefinition });
|
initFieldInputDraftValue({ recordId, fieldDefinition });
|
||||||
|
|
||||||
if (isDefined(customEditHotkeyScopeForField)) {
|
if (isDefined(customEditHotkeyScopeForField)) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope(customEditHotkeyScopeForField);
|
||||||
customEditHotkeyScopeForField.scope,
|
|
||||||
customEditHotkeyScopeForField.customScopes,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell);
|
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<
|
export type MultipleRecordPickerRecords<
|
||||||
CustomRecordForRecordPicker extends SingleRecordPickerRecord,
|
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 {
|
import {
|
||||||
SingleRecordPickerMenuItemsWithSearch,
|
SingleRecordPickerMenuItemsWithSearch,
|
||||||
SingleRecordPickerMenuItemsWithSearchProps,
|
SingleRecordPickerMenuItemsWithSearchProps,
|
||||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const SINGLE_RECORD_PICKER_LISTENER_ID = 'single-record-select';
|
||||||
|
|
||||||
export type SingleRecordPickerProps = {
|
export type SingleRecordPickerProps = {
|
||||||
width?: number;
|
width?: number;
|
||||||
componentInstanceId: string;
|
componentInstanceId: string;
|
||||||
@ -22,9 +24,9 @@ export const SingleRecordPicker = ({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
selectedRecordIds,
|
|
||||||
width = 200,
|
width = 200,
|
||||||
componentInstanceId,
|
componentInstanceId,
|
||||||
|
layoutDirection,
|
||||||
}: SingleRecordPickerProps) => {
|
}: SingleRecordPickerProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -41,11 +43,11 @@ export const SingleRecordPicker = ({
|
|||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
listenerId: 'single-record-select',
|
listenerId: SINGLE_RECORD_PICKER_LISTENER_ID,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordPickerComponentInstanceContext.Provider
|
<SingleRecordPickerComponentInstanceContext.Provider
|
||||||
value={{ instanceId: componentInstanceId }}
|
value={{ instanceId: componentInstanceId }}
|
||||||
>
|
>
|
||||||
<DropdownMenu ref={containerRef} width={width} data-select-disable>
|
<DropdownMenu ref={containerRef} width={width} data-select-disable>
|
||||||
@ -58,10 +60,10 @@ export const SingleRecordPicker = ({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
selectedRecordIds,
|
layoutDirection,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -2,10 +2,12 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Avatar, MenuItemSelectAvatar } from 'twenty-ui';
|
import { Avatar, MenuItemSelectAvatar } from 'twenty-ui';
|
||||||
|
|
||||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
|
|
||||||
type SingleRecordPickerMenuItemProps = {
|
type SingleRecordPickerMenuItemProps = {
|
||||||
record: SingleRecordPickerRecord;
|
record: SingleRecordPickerRecord;
|
||||||
@ -22,11 +24,20 @@ export const SingleRecordPickerMenuItem = ({
|
|||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
selectedRecord,
|
selectedRecord,
|
||||||
}: SingleRecordPickerMenuItemProps) => {
|
}: SingleRecordPickerMenuItemProps) => {
|
||||||
|
const recordPickerComponentInstanceId =
|
||||||
|
useAvailableComponentInstanceIdOrThrow(
|
||||||
|
SingleRecordPickerComponentInstanceContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectableListComponentInstanceId =
|
||||||
|
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||||
|
|
||||||
const { isSelectedItemIdSelector } = useSelectableList(
|
const { isSelectedItemIdSelector } = useSelectableList(
|
||||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
selectableListComponentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id));
|
const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectableItem itemId={record.id} key={record.id}>
|
<StyledSelectableItem itemId={record.id} key={record.id}>
|
||||||
<MenuItemSelectAvatar
|
<MenuItemSelectAvatar
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/components/SingleRecordPickerMenuItem';
|
import { SingleRecordPickerMenuItem } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem';
|
||||||
import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId';
|
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope';
|
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||||
import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord';
|
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 = {
|
export type SingleRecordPickerMenuItemsProps = {
|
||||||
EmptyIcon?: IconComponent;
|
EmptyIcon?: IconComponent;
|
||||||
@ -26,9 +31,12 @@ export type SingleRecordPickerMenuItemsProps = {
|
|||||||
selectedRecord?: SingleRecordPickerRecord;
|
selectedRecord?: SingleRecordPickerRecord;
|
||||||
hotkeyScope?: string;
|
hotkeyScope?: string;
|
||||||
isFiltered: boolean;
|
isFiltered: boolean;
|
||||||
shouldSelectEmptyOption?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SingleRecordPickerMenuItems = ({
|
export const SingleRecordPickerMenuItems = ({
|
||||||
EmptyIcon,
|
EmptyIcon,
|
||||||
emptyLabel,
|
emptyLabel,
|
||||||
@ -37,9 +45,8 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
selectedRecord,
|
selectedRecord,
|
||||||
hotkeyScope = RecordPickerHotkeyScope.RecordPicker,
|
hotkeyScope = SingleRecordPickerHotkeyScope.SingleRecordPicker,
|
||||||
isFiltered,
|
isFiltered,
|
||||||
shouldSelectEmptyOption,
|
|
||||||
}: SingleRecordPickerMenuItemsProps) => {
|
}: SingleRecordPickerMenuItemsProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -60,8 +67,16 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
isDefined(entity) && isNonEmptyString(entity.name),
|
isDefined(entity) && isNonEmptyString(entity.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recordPickerComponentInstanceId =
|
||||||
|
useAvailableComponentInstanceIdOrThrow(
|
||||||
|
SingleRecordPickerComponentInstanceContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectableListComponentInstanceId =
|
||||||
|
getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId);
|
||||||
|
|
||||||
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
|
const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList(
|
||||||
RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID,
|
selectableListComponentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelectedSelectNoneButton = useRecoilValue(
|
const isSelectedSelectNoneButton = useRecoilValue(
|
||||||
@ -79,17 +94,21 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
|
const selectableItemIds = recordsInDropdown.map((entity) => entity.id);
|
||||||
|
const [selectedRecordId, setSelectedRecordId] = useRecoilComponentStateV2(
|
||||||
|
singleRecordPickerSelectedIdComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<StyledContainer ref={containerRef}>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId={RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID}
|
selectableListId={selectableListComponentInstanceId}
|
||||||
selectableItemIdArray={selectableItemIds}
|
selectableItemIdArray={selectableItemIds}
|
||||||
hotkeyScope={hotkeyScope}
|
hotkeyScope={hotkeyScope}
|
||||||
onEnter={(itemId) => {
|
onEnter={(itemId) => {
|
||||||
const recordIndex = recordsInDropdown.findIndex(
|
const recordIndex = recordsInDropdown.findIndex(
|
||||||
(record) => record.id === itemId,
|
(record) => record.id === itemId,
|
||||||
);
|
);
|
||||||
|
setSelectedRecordId(itemId);
|
||||||
onRecordSelected(recordsInDropdown[recordIndex]);
|
onRecordSelected(recordsInDropdown[recordIndex]);
|
||||||
resetSelectedItem();
|
resetSelectedItem();
|
||||||
}}
|
}}
|
||||||
@ -107,10 +126,13 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
emptyLabel && (
|
emptyLabel && (
|
||||||
<MenuItemSelect
|
<MenuItemSelect
|
||||||
key={record.id}
|
key={record.id}
|
||||||
onClick={() => onRecordSelected()}
|
onClick={() => {
|
||||||
|
setSelectedRecordId(undefined);
|
||||||
|
onRecordSelected();
|
||||||
|
}}
|
||||||
LeftIcon={EmptyIcon}
|
LeftIcon={EmptyIcon}
|
||||||
text={emptyLabel}
|
text={emptyLabel}
|
||||||
selected={shouldSelectEmptyOption === true}
|
selected={isUndefined(selectedRecordId)}
|
||||||
hovered={isSelectedSelectNoneButton}
|
hovered={isSelectedSelectNoneButton}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -131,6 +153,6 @@ export const SingleRecordPickerMenuItems = ({
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
</div>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
SingleRecordPickerMenuItems,
|
SingleRecordPickerMenuItems,
|
||||||
SingleRecordPickerMenuItemsProps,
|
SingleRecordPickerMenuItemsProps,
|
||||||
} from '@/object-record/record-picker/components/SingleRecordPickerMenuItems';
|
} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems';
|
||||||
import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions';
|
import { useSingleRecordPickerRecords } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords';
|
||||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
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 { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { Placement } from '@floating-ui/react';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IconPlus } from 'twenty-ui';
|
import { IconPlus } from 'twenty-ui';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|
||||||
|
|
||||||
export type SingleRecordPickerMenuItemsWithSearchProps = {
|
export type SingleRecordPickerMenuItemsWithSearchProps = {
|
||||||
excludedRecordIds?: string[];
|
excludedRecordIds?: string[];
|
||||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordPickerInstanceId?: string;
|
recordPickerInstanceId?: string;
|
||||||
selectedRecordIds: string[];
|
layoutDirection?: RecordPickerLayoutDirection;
|
||||||
dropdownPlacement?: Placement | null;
|
|
||||||
} & Pick<
|
} & Pick<
|
||||||
SingleRecordPickerMenuItemsProps,
|
SingleRecordPickerMenuItemsProps,
|
||||||
| 'EmptyIcon'
|
| 'EmptyIcon'
|
||||||
@ -42,25 +40,23 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
onCreate,
|
onCreate,
|
||||||
onRecordSelected,
|
onRecordSelected,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
selectedRecordIds,
|
layoutDirection = 'search-bar-on-top',
|
||||||
dropdownPlacement,
|
|
||||||
}: SingleRecordPickerMenuItemsWithSearchProps) => {
|
}: SingleRecordPickerMenuItemsWithSearchProps) => {
|
||||||
const { handleSearchFilterChange } = useRecordSelectSearch();
|
const { handleSearchFilterChange } = useSingleRecordPickerSearch();
|
||||||
|
|
||||||
const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
|
const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||||
RecordPickerComponentInstanceContext,
|
SingleRecordPickerComponentInstanceContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||||
|
|
||||||
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
const recordPickerSearchFilter = useRecoilComponentValueV2(
|
||||||
recordPickerSearchFilterComponentState,
|
singleRecordPickerSearchFilterComponentState,
|
||||||
recordPickerInstanceId,
|
recordPickerInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { records } = useRecordPickerRecordsOptions({
|
const { records } = useSingleRecordPickerRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
selectedRecordIds,
|
|
||||||
excludedRecordIds,
|
excludedRecordIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,12 +68,9 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldDisplayDropdownMenuItems =
|
|
||||||
records.recordsToSelect.length + records.selectedRecords?.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{dropdownPlacement?.includes('end') && (
|
{layoutDirection === 'search-bar-on-bottom' && (
|
||||||
<>
|
<>
|
||||||
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
{isDefined(onCreate) && !hasObjectReadOnlyPermission && (
|
||||||
<DropdownMenuItemsContainer scrollable={false}>
|
<DropdownMenuItemsContainer scrollable={false}>
|
||||||
@ -85,22 +78,18 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
)}
|
)}
|
||||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||||
{shouldDisplayDropdownMenuItems && (
|
<SingleRecordPickerMenuItems
|
||||||
<SingleRecordPickerMenuItems
|
recordsToSelect={records.recordsToSelect}
|
||||||
recordsToSelect={records.recordsToSelect}
|
loading={records.loading}
|
||||||
loading={records.loading}
|
selectedRecord={records.selectedRecords?.[0]}
|
||||||
selectedRecord={records.selectedRecords?.[0]}
|
isFiltered={!!recordPickerSearchFilter}
|
||||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
{...{
|
||||||
hotkeyScope={recordPickerInstanceId}
|
EmptyIcon,
|
||||||
isFiltered={!!recordPickerSearchFilter}
|
emptyLabel,
|
||||||
{...{
|
onCancel,
|
||||||
EmptyIcon,
|
onRecordSelected,
|
||||||
emptyLabel,
|
}}
|
||||||
onCancel,
|
/>
|
||||||
onRecordSelected,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -109,26 +98,21 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
|||||||
autoFocus
|
autoFocus
|
||||||
role="combobox"
|
role="combobox"
|
||||||
/>
|
/>
|
||||||
{(dropdownPlacement?.includes('start') ||
|
{layoutDirection === 'search-bar-on-top' && (
|
||||||
isUndefinedOrNull(dropdownPlacement)) && (
|
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{shouldDisplayDropdownMenuItems && (
|
<SingleRecordPickerMenuItems
|
||||||
<SingleRecordPickerMenuItems
|
recordsToSelect={records.recordsToSelect}
|
||||||
recordsToSelect={records.recordsToSelect}
|
loading={records.loading}
|
||||||
loading={records.loading}
|
selectedRecord={records.selectedRecords?.[0]}
|
||||||
selectedRecord={records.selectedRecords?.[0]}
|
isFiltered={!!recordPickerSearchFilter}
|
||||||
shouldSelectEmptyOption={selectedRecordIds?.length === 0}
|
{...{
|
||||||
hotkeyScope={recordPickerInstanceId}
|
EmptyIcon,
|
||||||
isFiltered={!!recordPickerSearchFilter}
|
emptyLabel,
|
||||||
{...{
|
onCancel,
|
||||||
EmptyIcon,
|
onRecordSelected,
|
||||||
emptyLabel,
|
}}
|
||||||
onCancel,
|
/>
|
||||||
onRecordSelected,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
{records.recordsToSelect.length > 0 && isDefined(onCreate) && (
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
)}
|
)}
|
||||||
@ -10,8 +10,8 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
|||||||
import { allMockPersonRecords } from '~/testing/mock-data/people';
|
import { allMockPersonRecords } from '~/testing/mock-data/people';
|
||||||
import { sleep } from '~/utils/sleep';
|
import { sleep } from '~/utils/sleep';
|
||||||
|
|
||||||
import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker';
|
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||||
import { SingleRecordPickerRecord } from '../../types/SingleRecordPickerRecord';
|
import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||||
|
|
||||||
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
|
const records = allMockPersonRecords.map<SingleRecordPickerRecord>(
|
||||||
(person) => ({
|
(person) => ({
|
||||||
@ -34,7 +34,6 @@ const meta: Meta<typeof SingleRecordPicker> = {
|
|||||||
],
|
],
|
||||||
args: {
|
args: {
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||||
selectedRecordIds: [],
|
|
||||||
componentInstanceId: 'single-record-picker',
|
componentInstanceId: 'single-record-picker',
|
||||||
},
|
},
|
||||||
argTypes: {
|
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 { ChangeEvent } from 'react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch';
|
import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch';
|
||||||
import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext';
|
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
|
||||||
const instanceId = 'instanceId';
|
const instanceId = 'instanceId';
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<RecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
<SingleRecordPickerComponentInstanceContext.Provider value={{ instanceId }}>
|
||||||
<RecoilRoot>{children}</RecoilRoot>
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
</RecordPickerComponentInstanceContext.Provider>
|
</SingleRecordPickerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('useRecordSelectSearch', () => {
|
describe('useSingleRecordPickerRecords', () => {
|
||||||
it('should update searchFilter after change event', async () => {
|
it('should update searchFilter after change event', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const recordSelectSearchHook = useRecordSelectSearch(instanceId);
|
const recordSelectSearchHook = useSingleRecordPickerSearch(instanceId);
|
||||||
const internallyStoredFilter = useRecoilComponentValueV2(
|
const internallyStoredFilter = useRecoilComponentValueV2(
|
||||||
recordPickerSearchFilterComponentState,
|
singleRecordPickerSearchFilterComponentState,
|
||||||
instanceId,
|
instanceId,
|
||||||
);
|
);
|
||||||
return { recordSelectSearchHook, internallyStoredFilter };
|
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 { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||||
import { recordPickerPreselectedIdComponentState } from '@/object-record/record-picker/states/recordPickerPreselectedIdComponentState';
|
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||||
import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState';
|
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
export const useRecordSelectSearch = (
|
export const useSingleRecordPickerSearch = (
|
||||||
recordPickerComponentInstanceIdFromProps?: string,
|
recordPickerComponentInstanceIdFromProps?: string,
|
||||||
) => {
|
) => {
|
||||||
const recordPickerComponentInstanceId =
|
const recordPickerComponentInstanceId =
|
||||||
useAvailableComponentInstanceIdOrThrow(
|
useAvailableComponentInstanceIdOrThrow(
|
||||||
RecordPickerComponentInstanceContext,
|
SingleRecordPickerComponentInstanceContext,
|
||||||
recordPickerComponentInstanceIdFromProps,
|
recordPickerComponentInstanceIdFromProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
const setRecordPickerSearchFilter = useSetRecoilComponentStateV2(
|
||||||
recordPickerSearchFilterComponentState,
|
singleRecordPickerSearchFilterComponentState,
|
||||||
recordPickerComponentInstanceId,
|
recordPickerComponentInstanceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setRecordPickerPreselectedId = useSetRecoilComponentStateV2(
|
const setRecordPickerSelectedId = useSetRecoilComponentStateV2(
|
||||||
recordPickerPreselectedIdComponentState,
|
singleRecordPickerSelectedIdComponentState,
|
||||||
recordPickerComponentInstanceId,
|
recordPickerComponentInstanceId,
|
||||||
);
|
);
|
||||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||||
@ -33,14 +33,14 @@ export const useRecordSelectSearch = (
|
|||||||
|
|
||||||
const resetSearchFilter = () => {
|
const resetSearchFilter = () => {
|
||||||
debouncedSetSearchFilter('');
|
debouncedSetSearchFilter('');
|
||||||
setRecordPickerPreselectedId('');
|
setRecordPickerSelectedId(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchFilterChange = (
|
const handleSearchFilterChange = (
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
debouncedSetSearchFilter(event.currentTarget.value);
|
debouncedSetSearchFilter(event.currentTarget.value);
|
||||||
setRecordPickerPreselectedId('');
|
setRecordPickerSelectedId(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user