diff --git a/packages/twenty-front/src/modules/activities/hooks/useObjectRecordMultiSelectScopedStates.ts b/packages/twenty-front/src/modules/activities/hooks/useObjectRecordMultiSelectScopedStates.ts deleted file mode 100644 index b49c9aefa..000000000 --- a/packages/twenty-front/src/modules/activities/hooks/useObjectRecordMultiSelectScopedStates.ts +++ /dev/null @@ -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, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx deleted file mode 100644 index c6daa824a..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ /dev/null @@ -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({ - 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({ - 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(null); - - return ( - <> - - - - -
- -
- - ); -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx deleted file mode 100644 index be0cf8767..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx +++ /dev/null @@ -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 <>; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx deleted file mode 100644 index c06f7e4f5..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx +++ /dev/null @@ -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 <>; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetObjectRecordEffect.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetObjectRecordEffect.tsx deleted file mode 100644 index aee9ea49d..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetObjectRecordEffect.tsx +++ /dev/null @@ -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 <>; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index 2b0f2d052..90620554d 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -4,8 +4,8 @@ import { IconArrowUpRight, IconPencil } from 'twenty-ui'; import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; -import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode'; import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode'; +import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; @@ -18,6 +18,7 @@ import { RecordFieldInputScope } from '@/object-record/record-field/scopes/Recor import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer'; import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; +import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; type ActivityTargetsInlineCellProps = { @@ -38,6 +39,8 @@ export const ActivityTargetsInlineCell = ({ const { activityTargetObjectRecords } = useActivityTargetObjectRecords(activity); + const multipleRecordPickerInstanceId = `multiple-record-picker-target-${activity.id}`; + const { closeInlineCell } = useInlineCell(); const { fieldDefinition } = useContext(FieldContext); @@ -64,6 +67,12 @@ export const ActivityTargetsInlineCell = ({ const { openActivityTargetInlineCellEditMode } = useOpenActivityTargetInlineCellEditMode(); + const { updateActivityTargetFromInlineCell } = + useUpdateActivityTargetFromInlineCell({ + activityObjectNameSingular, + activityId: activity.id, + }); + return ( @@ -72,20 +81,27 @@ export const ActivityTargetsInlineCell = ({ {}} + onChange={(morphItem) => { + updateActivityTargetFromInlineCell({ + recordPickerInstanceId: multipleRecordPickerInstanceId, + morphItem, + activityTargetWithTargetRecords: + activityTargetObjectRecords, + }); + }} + onSubmit={() => { + closeInlineCell(); + }} /> ), label: 'Relations', @@ -97,7 +113,8 @@ export const ActivityTargetsInlineCell = ({ ), onOpenEditMode: () => { openActivityTargetInlineCellEditMode({ - recordPickerInstanceId: `record-picker-${activity.id}`, + recordPickerInstanceId: multipleRecordPickerInstanceId, + activityTargetObjectRecords, }); }, }} diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/MultipleObjectRecordOnClickOutsideEffect.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/MultipleObjectRecordOnClickOutsideEffect.tsx deleted file mode 100644 index 519771c92..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/MultipleObjectRecordOnClickOutsideEffect.tsx +++ /dev/null @@ -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; - 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 <>; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx b/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx deleted file mode 100644 index 8c187003f..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx +++ /dev/null @@ -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 }) => ( - - {children} - -); - -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}`, - ); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx b/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx deleted file mode 100644 index ea9a546f1..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/__tests__/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.test.tsx +++ /dev/null @@ -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 }) => ( - - {children} - -); - -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}`, - ); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts deleted file mode 100644 index 42211d086..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts +++ /dev/null @@ -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, - }; - }; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearch.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearch.ts deleted file mode 100644 index f205bb580..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearch.ts +++ /dev/null @@ -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( - multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY, - { - variables: { - search: searchFilterValue, - ...limitPerMetadataItem, - }, - skip: !isDefined(multiSelectSearchQueryForSelectedIds), - }, - ); - - return { - matchesSearchFilterObjectRecordsLoading, - matchesSearchFilterObjectRecordsQueryResult, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts deleted file mode 100644 index 619c9e7aa..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap.ts +++ /dev/null @@ -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, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchSelectedItemsQuery.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchSelectedItemsQuery.ts deleted file mode 100644 index 4e9869cfa..000000000 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useMultiObjectSearchSelectedItemsQuery.ts +++ /dev/null @@ -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( - multiSelectQueryForSelectedIds ?? EMPTY_QUERY, - { - variables: { - ...selectedIdFilterPerMetadataItem, - ...orderByFieldPerMetadataItem, - ...limitPerMetadataItem, - }, - skip: !isDefined(multiSelectQueryForSelectedIds), - }, - ); - - const { objectRecordForSelectArray: selectedObjectRecords } = - useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: selectedObjectRecordsQueryResult, - }); - - return { - selectedObjectRecordsLoading, - selectedObjectRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts index 0f2fc711a..a65a1751d 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode.ts @@ -1,14 +1,90 @@ +import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; +import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; +import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; +import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; +import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; +import { useRecoilCallback } from 'recoil'; + type OpenActivityTargetInlineCellEditModeProps = { recordPickerInstanceId: string; + activityTargetObjectRecords: ActivityTargetWithTargetRecord[]; }; export const useOpenActivityTargetInlineCellEditMode = () => { - const openActivityTargetInlineCellEditMode = ({ - recordPickerInstanceId, - }: OpenActivityTargetInlineCellEditModeProps) => { - // eslint-disable-next-line no-console - console.log('openActivityTargetInlineCellEditMode', recordPickerInstanceId); - }; + const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } = + useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID); + + const { performSearch: multipleRecordPickerPerformSearch } = + useMultipleRecordPickerPerformSearch(); + + const openActivityTargetInlineCellEditMode = useRecoilCallback( + ({ set, snapshot }) => + ({ + recordPickerInstanceId, + activityTargetObjectRecords, + }: OpenActivityTargetInlineCellEditModeProps) => { + const objectMetadataItems = snapshot + .getLoadable(objectMetadataItemsState) + .getValue() + .filter( + (objectMetadataItem) => + objectMetadataItem.isSearchable && + objectMetadataItem.nameSingular !== CoreObjectNameSingular.Task && + objectMetadataItem.nameSingular !== CoreObjectNameSingular.Note, + ); + + set( + multipleRecordPickerPickableMorphItemsComponentState.atomFamily({ + instanceId: recordPickerInstanceId, + }), + activityTargetObjectRecords.map((activityTargetObjectRecord) => ({ + recordId: activityTargetObjectRecord.targetObject.id, + objectMetadataId: + activityTargetObjectRecord.targetObjectMetadataItem.id, + isSelected: true, + isMatchingSearchFilter: true, + })), + ); + + set( + multipleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily( + { + instanceId: recordPickerInstanceId, + }, + ), + objectMetadataItems, + ); + + set( + multipleRecordPickerSearchFilterComponentState.atomFamily({ + instanceId: recordPickerInstanceId, + }), + '', + ); + + toggleRightDrawerClickOustideListener(false); + + multipleRecordPickerPerformSearch({ + multipleRecordPickerInstanceId: recordPickerInstanceId, + forceSearchFilter: '', + forceSearchableObjectMetadataItems: objectMetadataItems, + forcePickableMorphItems: activityTargetObjectRecords.map( + (activityTargetObjectRecord) => ({ + recordId: activityTargetObjectRecord.targetObject.id, + objectMetadataId: + activityTargetObjectRecord.targetObjectMetadataItem.id, + isSelected: true, + isMatchingSearchFilter: true, + }), + ), + }); + }, + [multipleRecordPickerPerformSearch, toggleRightDrawerClickOustideListener], + ); return { openActivityTargetInlineCellEditMode }; }; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts new file mode 100644 index 000000000..1ae590bc3 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell.ts @@ -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); + } + + setActivityFromStore((currentActivity) => { + if (isNull(currentActivity)) { + return null; + } + + return { + ...currentActivity, + [`${targetObjectName}Targets`]: activityTargetsAfterUpdate, + }; + }); + }, + [ + activityId, + activityObjectNameSingular, + createOneActivityTarget, + deleteOneActivityTarget, + setActivityFromStore, + ], + ); + + return { updateActivityTargetFromInlineCell }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useLimitPerMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useLimitPerMetadataItem.test.tsx index 2125c0946..01c0df841 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useLimitPerMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useLimitPerMetadataItem.test.tsx @@ -3,13 +3,13 @@ import { RecoilRoot } from 'recoil'; import { useLimitPerMetadataItem } from '@/object-metadata/hooks/useLimitPerMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; const instanceId = 'instanceId'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} - + ); describe('useLimitPerMetadataItem', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts index 9ae2d9fe0..816ed62f5 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts @@ -17,7 +17,7 @@ export const getLabelIdentifierFieldValue = ( } if (isDefined(labelIdentifierFieldMetadataItem?.name)) { - return String(record[labelIdentifierFieldMetadataItem.name]); + return record[labelIdentifierFieldMetadataItem.name]; } return ''; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts index c47872e56..efe8ac09d 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts @@ -12,7 +12,13 @@ export const getObjectRecordIdentifier = ({ objectMetadataItem, record, }: { - objectMetadataItem: ObjectMetadataItem; + objectMetadataItem: Pick< + ObjectMetadataItem, + | 'fields' + | 'labelIdentifierFieldMetadataId' + | 'nameSingular' + | 'imageIdentifierFieldMetadataId' + >; record: ObjectRecord; }): ObjectRecordIdentifier => { const labelIdentifierFieldMetadataItem = diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts index bd1bf73fa..0247b0e46 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedFindManyRecords.ts @@ -5,7 +5,7 @@ import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables'; import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; -import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult'; +import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult'; export const useCombinedFindManyRecords = ({ operationSignatures, @@ -22,7 +22,7 @@ export const useCombinedFindManyRecords = ({ operationSignatures, }); - const { data, loading } = useQuery( + const { data, loading } = useQuery( findManyQuery ?? EMPTY_QUERY, { skip, diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedGetTotalCount.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedGetTotalCount.ts index 882e826d9..c9bd9072b 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedGetTotalCount.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useCombinedGetTotalCount.ts @@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; -import { MultiObjectRecordQueryResult } from '@/object-record/multiple-objects/types/MultiObjectRecordQueryResult'; +import { CombinedFindManyRecordsQueryResult } from '@/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult'; export const useCombinedGetTotalCount = ({ objectMetadataItems, @@ -28,7 +28,7 @@ export const useCombinedGetTotalCount = ({ operationSignatures, }); - const { data } = useQuery( + const { data } = useQuery( findManyQuery ?? EMPTY_QUERY, { skip, diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts index a03678b38..4a1cbab8b 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts @@ -1,12 +1,8 @@ -import { gql } from '@apollo/client'; -import { isUndefined } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; -import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; -import { capitalize } from 'twenty-shared'; +import { generateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; export const useGenerateCombinedSearchRecordsQuery = ({ @@ -20,70 +16,8 @@ export const useGenerateCombinedSearchRecordsQuery = ({ return null; } - const filterPerMetadataItemArray = operationSignatures - .map( - ({ objectNameSingular }) => - `$filter${capitalize(objectNameSingular)}: ${capitalize( - objectNameSingular, - )}FilterInput`, - ) - .join(', '); - - const limitPerMetadataItemArray = operationSignatures - .map( - ({ objectNameSingular }) => - `$limit${capitalize(objectNameSingular)}: Int`, - ) - .join(', '); - - const queryKeyWithObjectMetadataItemArray = operationSignatures.map( - (queryKey) => { - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === queryKey.objectNameSingular, - ); - - if (isUndefined(objectMetadataItem)) { - throw new Error( - `Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`, - ); - } - - return { ...queryKey, objectMetadataItem }; - }, - ); - - const filteredQueryKeyWithObjectMetadataItemArray = - queryKeyWithObjectMetadataItemArray.filter( - ({ objectMetadataItem }) => objectMetadataItem.isSearchable, - ); - - return gql` - query CombinedSearchRecords( - ${filterPerMetadataItemArray}, - ${limitPerMetadataItemArray}, - $search: String, - ) { - ${filteredQueryKeyWithObjectMetadataItemArray - .map( - ({ objectMetadataItem }) => - `${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize( - objectMetadataItem.nameSingular, - )}, - limit: $limit${capitalize(objectMetadataItem.nameSingular)}, - searchInput: $search - ){ - edges { - node ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems: objectMetadataItems, - objectMetadataItem, - })} - cursor - } - totalCount - }`, - ) - .join('\n')} - } - `; + return generateCombinedSearchRecordsQuery({ + objectMetadataItems, + operationSignatures, + }); }; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/types/MultiObjectRecordQueryResult.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult.ts similarity index 73% rename from packages/twenty-front/src/modules/object-record/multiple-objects/types/MultiObjectRecordQueryResult.ts rename to packages/twenty-front/src/modules/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult.ts index 91243623c..4c44d00c8 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/types/MultiObjectRecordQueryResult.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/types/CombinedFindManyRecordsQueryResult.ts @@ -1,5 +1,5 @@ import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -export type MultiObjectRecordQueryResult = { +export type CombinedFindManyRecordsQueryResult = { [namePlural: string]: RecordGqlConnection; }; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/formatMultiObjectRecordSearchResults.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/formatMultiObjectRecordSearchResults.ts deleted file mode 100644 index 2af506a1a..000000000 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/formatMultiObjectRecordSearchResults.ts +++ /dev/null @@ -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); -}; diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts new file mode 100644 index 000000000..96b0edba5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/utils/generateCombinedSearchRecordsQuery.ts @@ -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')} + } + `; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx index 1de032728..37da330e6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx @@ -7,8 +7,8 @@ import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldM import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; -import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; @@ -91,7 +91,7 @@ export const ObjectFilterDropdownBooleanSelect = () => { option.toString())} - hotkeyScope={RelationPickerHotkeyScope.RelationPicker} + hotkeyScope={SingleRecordPickerHotkeyScope.SingleRecordPicker} onEnter={(itemId) => { handleOptionSelect(itemId === 'true'); }} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index b4e5c6b09..f7505667e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -15,7 +15,7 @@ import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/i import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { findDuplicateRecordFilterInNonAdvancedRecordFilters } from '@/object-record/record-filter/utils/findDuplicateRecordFilterInNonAdvancedRecordFilters'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; -import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; @@ -87,7 +87,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); if (filterType === 'RELATION' || filterType === 'SELECT') { - setHotkeyScope(RecordPickerHotkeyScope.RecordPicker); + setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker); } const defaultOperand = getRecordFilterOperands({ diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index 0041474a7..ce6935a89 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -18,9 +18,8 @@ import { objectFilterDropdownSearchInputComponentState } from '@/object-record/o import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; -import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; -import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -102,7 +101,7 @@ export const ObjectFilterDropdownOptionSelect = () => { closeDropdown(); resetSelectedItem(); }, - RelationPickerHotkeyScope.RelationPicker, + SingleRecordPickerHotkeyScope.SingleRecordPicker, [closeDropdown, resetSelectedItem], ); @@ -165,7 +164,7 @@ export const ObjectFilterDropdownOptionSelect = () => { { const option = optionsInDropdown.find((option) => option.id === itemId); if (isDefined(option)) { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 44d6dd68e..698991048 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -12,7 +12,7 @@ import { selectedFilterComponentState } from '@/object-record/object-filter-drop import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; -import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; import { SelectableItem } from '@/object-record/select/types/SelectableItem'; @@ -232,7 +232,7 @@ export const ObjectFilterDropdownRecordSelect = ({ )} !filteredSelectedItems.some((selected) => selected.id === item.id), diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts index 8cbb31499..7f4932f16 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterUsedInDropdown.ts @@ -8,7 +8,7 @@ import { selectedOperandInDropdownComponentState } from '@/object-record/object- import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { useApplyRecordFilter } from '@/object-record/record-filter/hooks/useApplyRecordFilter'; import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands'; -import { RecordPickerHotkeyScope } from '@/object-record/record-picker/types/RecordPickerHotkeyScope'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -67,7 +67,7 @@ export const useSelectFilterUsedInDropdown = (componentInstanceId?: string) => { fieldMetadataItem.type === 'RELATION' || fieldMetadataItem.type === 'SELECT' ) { - setHotkeyScope(RecordPickerHotkeyScope.RecordPicker); + setHotkeyScope(SingleRecordPickerHotkeyScope.SingleRecordPicker); } const filterType = getFilterTypeFromFieldType(fieldMetadataItem.type); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx index 7ea5e97db..4e3d4f8e4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunity.tsx @@ -2,8 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector'; -import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; +import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; @@ -68,22 +67,15 @@ export const RecordBoardColumnNewOpportunity = ({ <> {newRecord.isCreating && newRecord.position === position && ( - - handleCreateSuccess(position, columnId, false)} - onRecordSelected={(company) => - company ? handleEntitySelect(position, company) : null - } - objectNameSingular={CoreObjectNameSingular.Company} - selectedRecordIds={[]} - onCreate={createCompanyOpportunityAndOpenRightDrawer} - /> - + handleCreateSuccess(position, columnId, false)} + onRecordSelected={(company) => + company ? handleEntitySelect(position, company) : null + } + objectNameSingular={CoreObjectNameSingular.Company} + onCreate={createCompanyOpportunityAndOpenRightDrawer} + /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts index fb30fefdb..8a96a6a7e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts @@ -1,9 +1,9 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector'; -import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope'; -import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useCallback, useContext } from 'react'; import { RecoilState, useRecoilCallback } from 'recoil'; @@ -26,7 +26,7 @@ export const useAddNewCard = ({ const columnContext = useContext(RecordBoardColumnContext); const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } = useContext(RecordBoardContext); - const { resetSearchFilter } = useRecordSelectSearch( + const { resetSearchFilter } = useSingleRecordPickerSearch( recordPickerComponentInstanceId, ); @@ -139,7 +139,7 @@ export const useAddNewCard = ({ addNewItem(set, columnDefinitionId, position, isOpportunity); if (isOpportunity) { setHotkeyScopeAndMemorizePreviousScope( - RelationPickerHotkeyScope.RelationPicker, + SingleRecordPickerHotkeyScope.SingleRecordPicker, ); } else { createRecord(labelIdentifier, labelValue, position, isOpportunity); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts index 3bdbc7eed..49251ad9e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts @@ -1,4 +1,4 @@ -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; export type NewCard = { diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index f486a5a68..7875ecfe9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -17,6 +17,7 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; @@ -71,108 +72,114 @@ export const FieldInput = ({ const { fieldDefinition } = useContext(FieldContext); return ( - - {isFieldRelationToOneObject(fieldDefinition) ? ( - - ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - - ) : isFieldPhones(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldText(fieldDefinition) ? ( - - ) : isFieldEmails(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldFullName(fieldDefinition) ? ( - - ) : isFieldDateTime(fieldDefinition) ? ( - - ) : isFieldDate(fieldDefinition) ? ( - - ) : isFieldNumber(fieldDefinition) ? ( - - ) : isFieldLinks(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldCurrency(fieldDefinition) ? ( - - ) : isFieldBoolean(fieldDefinition) ? ( - - ) : isFieldRating(fieldDefinition) ? ( - - ) : isFieldSelect(fieldDefinition) ? ( - - ) : isFieldMultiSelect(fieldDefinition) ? ( - - ) : isFieldAddress(fieldDefinition) ? ( - - ) : isFieldRawJson(fieldDefinition) ? ( - - ) : isFieldArray(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : ( - <> - )} - + + {isFieldRelationToOneObject(fieldDefinition) ? ( + + ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( + + ) : isFieldPhones(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldText(fieldDefinition) ? ( + + ) : isFieldEmails(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldFullName(fieldDefinition) ? ( + + ) : isFieldDateTime(fieldDefinition) ? ( + + ) : isFieldDate(fieldDefinition) ? ( + + ) : isFieldNumber(fieldDefinition) ? ( + + ) : isFieldLinks(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldCurrency(fieldDefinition) ? ( + + ) : isFieldBoolean(fieldDefinition) ? ( + + ) : isFieldRating(fieldDefinition) ? ( + + ) : isFieldSelect(fieldDefinition) ? ( + + ) : isFieldMultiSelect(fieldDefinition) ? ( + + ) : isFieldAddress(fieldDefinition) ? ( + + ) : isFieldRawJson(fieldDefinition) ? ( + + ) : isFieldArray(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : ( + <> + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts new file mode 100644 index 000000000..7764b2ba5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useOpenFieldInputEditMode.ts @@ -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; + 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: () => {}, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index 7e04ec3ad..5a190391a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -31,7 +31,6 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue'; import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName'; import { FieldContext } from '../contexts/FieldContext'; import { isFieldBoolean } from '../types/guards/isFieldBoolean'; @@ -156,13 +155,12 @@ export const usePersistField = () => { ); if (fieldIsRelationToOneObject) { - const value = valueToPersist as SingleRecordPickerRecord; updateRecord?.({ variables: { where: { id: recordId }, updateOneRecordInput: { [getForeignKeyNameFromRelationFieldName(fieldName)]: - value?.id ?? null, + valueToPersist?.id ?? null, }, }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts index 1904588e1..9781fcfac 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts @@ -7,14 +7,12 @@ import { FieldRelationValue } from '@/object-record/record-field/types/FieldMeta import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { FieldContext } from '../../contexts/FieldContext'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldRelation } from '../../types/guards/isFieldRelation'; -export const useRelationField = < - T extends SingleRecordPickerRecord | SingleRecordPickerRecord[], ->() => { +export const useRelationField = () => { const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext); const button = useGetButtonIcon(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index 285c50bd2..899dbac7a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -2,14 +2,14 @@ import { useContext } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; +import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; +import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RelationFromManyFieldInputProps = { onSubmit?: FieldInputEvent; @@ -19,10 +19,9 @@ export const RelationFromManyFieldInput = ({ onSubmit, }: RelationFromManyFieldInputProps) => { const { fieldDefinition, recordId } = useContext(FieldContext); - const recordPickerInstanceId = `record-picker-${fieldDefinition.fieldMetadataId}`; - const { updateRelation } = useUpdateRelationFromManyFieldInput({ - scopeId: recordPickerInstanceId, - }); + const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`; + + const { updateRelation } = useUpdateRelationFromManyFieldInput(); const handleSubmit = () => { onSubmit?.(() => {}); @@ -50,19 +49,22 @@ export const RelationFromManyFieldInput = ({ recordId, }); + const layoutDirection = useRecoilComponentValueV2( + recordFieldInputLayoutDirectionComponentState, + ); + return ( - <> - - - - - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx deleted file mode 100644 index c349a808a..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx +++ /dev/null @@ -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(); - 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 <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPicker.tsx deleted file mode 100644 index 7a0cae378..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPicker.tsx +++ /dev/null @@ -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; -}; - -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 ( - <> - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPickerInitialValueEffect.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPickerInitialValueEffect.tsx deleted file mode 100644 index 130ee18b8..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationPickerInitialValueEffect.tsx +++ /dev/null @@ -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 <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx index 00e8f6d5d..a1ea4eb43 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationToOneFieldInput.tsx @@ -1,8 +1,15 @@ -import { RelationPicker } from '@/object-record/record-field/meta-types/input/components/RelationPicker'; import { usePersistField } from '../../../hooks/usePersistField'; import { useRelationField } from '../../hooks/useRelationField'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; +import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; +import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; +import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { IconForbid } from 'twenty-ui'; import { FieldInputEvent } from './DateTimeFieldInput'; export type RelationToOneFieldInputProps = { @@ -14,22 +21,64 @@ export const RelationToOneFieldInput = ({ onSubmit, onCancel, }: RelationToOneFieldInputProps) => { - const { fieldDefinition, initialSearchValue, fieldValue } = - useRelationField(); + const { fieldDefinition, recordId } = useRelationField(); const persistField = usePersistField(); - const handleSubmit = (newEntity: SingleRecordPickerRecord | null) => { - onSubmit?.(() => persistField(newEntity?.record ?? null)); - }; + const recordPickerInstanceId = `relation-to-one-field-input-${recordId}-${fieldDefinition.metadata.fieldName}`; + + const handleRecordSelected = ( + selectedRecord: SingleRecordPickerRecord | null | undefined, + ) => onSubmit?.(() => persistField(selectedRecord?.record ?? null)); + + const { objectMetadataItem: relationObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + }); + + const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( + ({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId, + ); + + const { createNewRecordAndOpenRightDrawer } = + useAddNewRecordAndOpenRightDrawer({ + relationObjectMetadataNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + relationObjectMetadataItem, + relationFieldMetadataItem, + recordId, + }); + + const layoutDirection = useRecoilComponentValueV2( + recordFieldInputLayoutDirectionComponentState, + ); + + const isLoading = useRecoilComponentValueV2( + recordFieldInputLayoutDirectionLoadingComponentState, + ); + + if (isLoading) { + return <>; + } return ( - ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 564f103ba..e367869db 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -18,7 +18,10 @@ import { } from '~/testing/mock-data/users'; import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { getCanvasElementForDropdownTesting } from 'twenty-ui'; import { RelationToOneFieldInput, @@ -30,11 +33,21 @@ const RelationWorkspaceSetterEffect = () => { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); + const setRecordFieldInputLayoutDirectionLoading = + useSetRecoilComponentStateV2( + recordFieldInputLayoutDirectionLoadingComponentState, + 'relation-to-one-field-input-123-Relation', + ); useEffect(() => { setCurrentWorkspace(mockCurrentWorkspace); setCurrentWorkspaceMember(mockedWorkspaceMemberData); - }, [setCurrentWorkspace, setCurrentWorkspaceMember]); + setRecordFieldInputLayoutDirectionLoading(false); + }, [ + setCurrentWorkspace, + setCurrentWorkspaceMember, + setRecordFieldInputLayoutDirectionLoading, + ]); return <>; }; @@ -74,12 +87,18 @@ const RelationToOneFieldInputWithContext = ({ }} recordId={recordId} > - - - - + + + + +
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts index 296ee39bb..effcfecf4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts @@ -1,6 +1,7 @@ import { useSetRecoilState } from 'recoil'; import { v4 } from 'uuid'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; @@ -10,9 +11,11 @@ import { viewableRecordIdState } from '@/object-record/record-right-drawer/state import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-shared'; import { IconEye } from 'twenty-ui'; import { + FeatureFlagKey, FieldMetadataType, RelationDefinitionType, } from '~/generated-metadata/graphql'; @@ -45,6 +48,10 @@ export const useAddNewRecordAndOpenRightDrawer = ({ }); const { openRightDrawer } = useRightDrawer(); + const { openRecordInCommandMenu } = useCommandMenu(); + const isCommandMenuEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, + ); if ( relationObjectMetadataNameSingular === 'workspaceMember' || @@ -110,10 +117,18 @@ export const useAddNewRecordAndOpenRightDrawer = ({ setViewableRecordId(newRecordId); setViewableRecordNameSingular(relationObjectMetadataNameSingular); - openRightDrawer(RightDrawerPages.ViewRecord, { - title: 'View Record', - Icon: IconEye, - }); + + if (isCommandMenuEnabled) { + openRecordInCommandMenu({ + recordId: newRecordId, + objectNameSingular: relationObjectMetadataNameSingular, + }); + } else { + openRightDrawer(RightDrawerPages.ViewRecord, { + title: 'View Record', + Icon: IconEye, + }); + } }, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx new file mode 100644 index 000000000..c83cf9f69 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput.tsx @@ -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>( + 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 }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx new file mode 100644 index 000000000..567fbb919 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput.tsx @@ -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>( + recordStoreFamilySelector({ + recordId, + fieldName, + }), + ) + .getValue(); + + if (isDefined(fieldValue)) { + set( + singleRecordPickerSelectedIdComponentState.atomFamily({ + instanceId: recordPickerInstanceId, + }), + fieldValue.id, + ); + } + }, + [], + ); + + return { openRelationToOneFieldInput }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx index 1c0f99ac8..c074b1c31 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput.tsx @@ -4,16 +4,12 @@ import { useRecoilCallback } from 'recoil'; import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord'; import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; +import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -export const useUpdateRelationFromManyFieldInput = ({ - scopeId, -}: { - scopeId: string; -}) => { +export const useUpdateRelationFromManyFieldInput = () => { const { recordId, fieldDefinition } = useContext(FieldContext); assertFieldMetadata( @@ -41,49 +37,21 @@ export const useUpdateRelationFromManyFieldInput = ({ }); const updateRelation = useRecoilCallback( - ({ snapshot, set }) => - async (objectRecordId: string) => { - const previouslyCheckedRecordsIds = snapshot - .getLoadable( - objectRecordMultiSelectCheckedRecordsIdsComponentState({ - scopeId, - }), - ) - .getValue(); - - const isNewlySelected = - !previouslyCheckedRecordsIds.includes(objectRecordId); - if (isNewlySelected) { - set( - objectRecordMultiSelectCheckedRecordsIdsComponentState({ - scopeId, - }), - (prev) => [...prev, objectRecordId], - ); - } else { - set( - objectRecordMultiSelectCheckedRecordsIdsComponentState({ - scopeId, - }), - (prev) => prev.filter((id) => id !== objectRecordId), - ); - } - - if (isNewlySelected) { - await updateOneRecordAndAttachRelations({ - recordId, - relatedRecordId: objectRecordId, - }); - } else { - await updateOneRecordAndDetachRelations({ - recordId, - relatedRecordId: objectRecordId, - }); - } - }, + () => async (morphItem: RecordPickerPickableMorphItem) => { + if (morphItem.isSelected) { + await updateOneRecordAndAttachRelations({ + recordId, + relatedRecordId: morphItem.recordId, + }); + } else { + await updateOneRecordAndDetachRelations({ + recordId, + relatedRecordId: morphItem.recordId, + }); + } + }, [ recordId, - scopeId, updateOneRecordAndAttachRelations, updateOneRecordAndDetachRelations, ], diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope.ts index bd9c434ed..eb728eb85 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope.ts @@ -1,4 +1,3 @@ export enum RelationPickerHotkeyScope { - RelationPicker = 'relation-picker', AddNew = 'add-new', } diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/activityTargetObjectRecordFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/activityTargetObjectRecordFamilyState.ts deleted file mode 100644 index 45d54e009..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/activityTargetObjectRecordFamilyState.ts +++ /dev/null @@ -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 }, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext.ts new file mode 100644 index 000000000..555c50fc8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext.ts @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState.ts deleted file mode 100644 index 4f51db21d..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const objectRecordMultiSelectCheckedRecordsIdsComponentState = - createComponentState({ - key: 'objectRecordMultiSelectCheckedRecordsIdsComponentState', - defaultValue: [], - }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts deleted file mode 100644 index e4150ae02..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts +++ /dev/null @@ -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({ - key: 'objectRecordMultiSelectComponentFamilyState', - defaultValue: undefined, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts deleted file mode 100644 index bfaebeaa8..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts +++ /dev/null @@ -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({ - key: 'objectRecordMultiSelectMatchesFilterRecordsIdsComponentState', - defaultValue: [], - }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState.ts new file mode 100644 index 000000000..a6fa001bc --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState.ts @@ -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({ + key: 'recordFieldInputLayoutDirectionComponentState', + defaultValue: 'upward', + componentInstanceContext: RecordFieldComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState.ts new file mode 100644 index 000000000..a4299cae5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState.ts @@ -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({ + key: 'recordFieldInputLayoutDirectionLoadingComponentState', + defaultValue: true, + componentInstanceContext: RecordFieldComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/recordMultiSelectIsLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/recordMultiSelectIsLoadingComponentState.ts deleted file mode 100644 index 880207ecb..000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/states/recordMultiSelectIsLoadingComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordMultiSelectIsLoadingComponentState = - createComponentState({ - key: 'recordMultiSelectIsLoadingComponentState', - defaultValue: false, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputLayoutDirection.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputLayoutDirection.ts new file mode 100644 index 000000000..ffeeff1a9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputLayoutDirection.ts @@ -0,0 +1 @@ +export type FieldInputLayoutDirection = 'upward' | 'downward'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 5d221e6bf..7aca41823 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -2,7 +2,7 @@ import { ThemeColor } from 'twenty-ui'; import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ConnectedAccountProvider } from 'twenty-shared'; import * as z from 'zod'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; @@ -260,9 +260,9 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number] | null; export type FieldSelectValue = string | null; export type FieldMultiSelectValue = string[] | null; -export type FieldRelationToOneValue = SingleRecordPickerRecord | null; +export type FieldRelationToOneValue = ObjectRecord | null; -export type FieldRelationFromManyValue = SingleRecordPickerRecord[] | []; +export type FieldRelationFromManyValue = ObjectRecord[]; export type FieldRelationValue< T extends FieldRelationToOneValue | FieldRelationFromManyValue, diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts index 57468bba4..b6a07c194 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts @@ -2,10 +2,10 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { FieldDefinition } from '../FieldDefinition'; -import { FieldMetadata } from '../FieldMetadata'; +import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata'; export const isFieldRelationFromManyObjects = ( field: Pick, 'type' | 'metadata'>, -): field is FieldDefinition => +): field is FieldDefinition => isFieldRelation(field) && field.metadata.relationType === RelationDefinitionType.ONE_TO_MANY; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 53ae070c4..156040b61 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -8,17 +8,24 @@ import { FieldFocusContextProvider } from '@/object-record/record-field/contexts import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; -import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { useInlineCell } from '../hooks/useInlineCell'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; +import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode'; import { FieldInputClickOutsideEvent } from '@/object-record/record-field/meta-types/input/components/DateTimeFieldInput'; -import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope'; +import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; +import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; +import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope'; +import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope'; +import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState'; import { useRecoilCallback } from 'recoil'; +import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { RecordInlineCellContainer } from './RecordInlineCellContainer'; import { RecordInlineCellContext, @@ -39,6 +46,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => { onOpenEditMode, onCloseEditMode, } = useContext(FieldContext); + const buttonIcon = useGetButtonIcon(); const isFieldInputOnly = useIsFieldInputOnly(); @@ -101,13 +109,40 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => { ); const { getIcon } = useIcons(); + const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode(); + + // TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope + const computedHotkeyScope = ( + columnDefinition: FieldDefinition, + ) => { + if (isFieldRelation(columnDefinition)) { + if ( + columnDefinition.metadata.relationType === + RelationDefinitionType.MANY_TO_ONE + ) { + return SingleRecordPickerHotkeyScope.SingleRecordPicker; + } + + if ( + columnDefinition.metadata.relationType === + RelationDefinitionType.ONE_TO_MANY + ) { + return MultipleRecordPickerHotkeyScope.MultipleRecordPicker; + } + + return SingleRecordPickerHotkeyScope.SingleRecordPicker; + } + + if (isFieldSelect(columnDefinition)) { + return SelectFieldHotkeyScope.SelectField; + } + + return undefined; + }; const RecordInlineCellContextValue: RecordInlineCellContextProps = { readonly: isFieldReadOnly, buttonIcon: buttonIcon, - customEditHotkeyScope: isFieldRelation(fieldDefinition) - ? { scope: RelationPickerHotkeyScope.RelationPicker } - : undefined, IconLabel: fieldDefinition.iconName ? getIcon(fieldDefinition.iconName) : undefined, @@ -135,8 +170,10 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => { isDisplayModeFixHeight: isDisplayModeFixHeight, editModeContentOnly: isFieldInputOnly, loading: loading, - onOpenEditMode, - onCloseEditMode, + customEditHotkeyScope: computedHotkeyScope(fieldDefinition), + onOpenEditMode: + onOpenEditMode ?? (() => openFieldInput({ fieldDefinition, recordId })), + onCloseEditMode: onCloseEditMode ?? (() => closeFieldInput()), }; return ( diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContext.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContext.tsx index e3f31cba8..a4c1367bf 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContext.tsx @@ -1,4 +1,3 @@ -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { createContext, ReactElement, useContext } from 'react'; import { IconComponent } from 'twenty-ui'; @@ -12,7 +11,7 @@ export type RecordInlineCellContextProps = { editModeContent?: ReactElement; editModeContentOnly?: boolean; displayModeContent?: ReactElement; - customEditHotkeyScope?: HotkeyScope; + customEditHotkeyScope?: string; isDisplayModeFixHeight?: boolean; disableHoverEffect?: boolean; loading?: boolean; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx index f284e47bc..2d7a35a74 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx @@ -1,7 +1,18 @@ +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; +import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; +import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; -import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; +import { + MiddlewareState, + autoUpdate, + flip, + offset, + useFloating, +} from '@floating-ui/react'; import { useContext } from 'react'; import { createPortal } from 'react-dom'; @@ -24,6 +35,33 @@ export const RecordInlineCellEditMode = ({ children, }: RecordInlineCellEditModeProps) => { const { isCentered } = useContext(RecordInlineCellContext); + const { recordId, fieldDefinition } = useContext(FieldContext); + + const instanceId = getRecordFieldInputId( + recordId, + fieldDefinition?.metadata?.fieldName, + ); + + const setFieldInputLayoutDirection = useSetRecoilComponentStateV2( + recordFieldInputLayoutDirectionComponentState, + instanceId, + ); + + const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2( + recordFieldInputLayoutDirectionLoadingComponentState, + instanceId, + ); + + const setFieldInputLayoutDirectionMiddleware = { + name: 'middleware', + fn: async (state: MiddlewareState) => { + setFieldInputLayoutDirection( + state.placement.startsWith('bottom') ? 'downward' : 'upward', + ); + setFieldInputLayoutDirectionLoading(false); + return {}; + }, + }; const { refs, floatingStyles } = useFloating({ placement: isCentered ? 'bottom' : 'bottom-start', @@ -40,6 +78,7 @@ export const RecordInlineCellEditMode = ({ crossAxis: -5, }, ), + setFieldInputLayoutDirectionMiddleware, ], whileElementsMounted: autoUpdate, }); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts index 02deec84c..c6df67ec6 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/hooks/useInlineCell.ts @@ -3,7 +3,6 @@ import { useRecoilState } from 'recoil'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { isDefined } from 'twenty-shared'; import { useInitDraftValueV2 } from '@/object-record/record-field/hooks/useInitDraftValueV2'; @@ -48,16 +47,13 @@ export const useInlineCell = () => { goBackToPreviousDropdownFocusId(); }; - const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => { + const openInlineCell = (customEditHotkeyScopeForField?: string) => { onOpenEditMode?.(); setIsInlineCellInEditMode(true); initFieldInputDraftValue({ recordId, fieldDefinition }); if (isDefined(customEditHotkeyScopeForField)) { - setHotkeyScopeAndMemorizePreviousScope( - customEditHotkeyScopeForField.scope, - customEditHotkeyScopeForField.customScopes, - ); + setHotkeyScopeAndMemorizePreviousScope(customEditHotkeyScopeForField); } else { setHotkeyScopeAndMemorizePreviousScope(InlineCellHotkeyScope.InlineCell); } diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPicker.tsx deleted file mode 100644 index 0cac2d6dc..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPicker.tsx +++ /dev/null @@ -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(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) => { - setSearchFilter(event.currentTarget.value); - }, - [setSearchFilter], - ); - - // TODO: refactor this in a separate component - const results = ( - - { - onChange?.(selectedId); - resetSelectedItem(); - }} - > - {objectRecordsIdsMultiSelect?.map((recordId) => { - return ( - { - onChange?.(recordId); - resetSelectedItem(); - }} - /> - ); - })} - - - ); - - const createNewButton = isDefined(onCreate) && ( - onCreate?.(recordPickerSearchFilter)} - LeftIcon={IconPlus} - text="Add New" - /> - ); - - return ( - - - {dropdownPlacement?.includes('end') && ( - <> - {isDefined(onCreate) && !hasObjectReadOnlyPermission && ( - - {createNewButton} - - )} - - {objectRecordsIdsMultiSelect?.length > 0 && results} - {recordMultiSelectIsLoading && !recordPickerSearchFilter && ( - <> - - - - )} - {objectRecordsIdsMultiSelect?.length > 0 && ( - - )} - - )} - - {(dropdownPlacement?.includes('start') || - isUndefinedOrNull(dropdownPlacement)) && ( - <> - - {recordMultiSelectIsLoading && !recordPickerSearchFilter && ( - <> - - - - )} - {objectRecordsIdsMultiSelect?.length > 0 && results} - {objectRecordsIdsMultiSelect?.length > 0 && ( - - )} - {isDefined(onCreate) && ( - - {createNewButton} - - )} - - )} - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPickerMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPickerMenuItem.tsx deleted file mode 100644 index 71d10d812..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/MultipleRecordPickerMenuItem.tsx +++ /dev/null @@ -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 ( - - handleSelectChange()} - isKeySelected={isSelectedByKeyboard} - selected={selected} - avatar={ - - } - text={recordIdentifier.name} - /> - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerClickOutsideListenerId.ts b/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerClickOutsideListenerId.ts deleted file mode 100644 index ec32e3fa3..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerClickOutsideListenerId.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID = - 'record-picker-click-outside-listener'; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId.ts b/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId.ts deleted file mode 100644 index d41ddf475..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID = - 'record-picker-selectable-list-component-instance-id'; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts new file mode 100644 index 000000000..c0c53b30e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetRecordAndObjectMetadataItemFromRecordId.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerRecordsOptions.ts b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerRecordsOptions.ts deleted file mode 100644 index 9c21f8572..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerRecordsOptions.ts +++ /dev/null @@ -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 }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx new file mode 100644 index 000000000..298adb56a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker.tsx @@ -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(null); + + const handleCreateNewButtonClick = useRecoilCallback( + ({ snapshot }) => { + return () => { + const recordPickerSearchFilter = snapshot + .getLoadable(multipleRecordPickerSearchFilterState) + .getValue(); + onCreate?.(recordPickerSearchFilter); + }; + }, + [multipleRecordPickerSearchFilterState, onCreate], + ); + + const createNewButton = isDefined(onCreate) && ( + + ); + + return ( + + + + {layoutDirection === 'search-bar-on-bottom' && ( + <> + {isDefined(onCreate) && !hasObjectReadOnlyPermission && ( + + {createNewButton} + + )} + + {itemsLength > 0 && ( + + )} + {multipleRecordPickerIsLoading && ( + <> + + + + )} + {itemsLength > 0 && } + + )} + + {layoutDirection === 'search-bar-on-top' && ( + <> + + {multipleRecordPickerIsLoading && ( + <> + + + + )} + {itemsLength > 0 && ( + + )} + {itemsLength > 0 && } + {isDefined(onCreate) && ( + + {createNewButton} + + )} + + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx new file mode 100644 index 000000000..433939b42 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItem.tsx @@ -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 ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx new file mode 100644 index 000000000..1f78460af --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItemContent.tsx @@ -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 ( + + handleSelectChange(isSelected)} + isKeySelected={isSelectedByKeyboard} + selected={isRecordSelectedWithObjectItem} + avatar={ + + } + text={recordIdentifier.name} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx new file mode 100644 index 000000000..9d23d08f0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerMenuItems.tsx @@ -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 ( + + + {pickableRecordIds.map((recordId) => { + return ( + { + handleChange(morphItem); + onChange?.(morphItem); + resetSelectedItem(); + }} + /> + ); + })} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect.tsx new file mode 100644 index 000000000..5b6348178 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect.tsx @@ -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; + onClickOutside: () => void; +}) => { + useListenClickOutside({ + refs: [containerRef], + callback: (event) => { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + + onClickOutside(); + }, + listenerId: MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID, + }); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput.tsx b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput.tsx new file mode 100644 index 000000000..42f222787 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput.tsx @@ -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) => { + setRecordPickerSearchFilter(event.currentTarget.value); + performSearch({ + multipleRecordPickerInstanceId: componentInstanceId, + forceSearchFilter: event.currentTarget.value, + }); + }, + [componentInstanceId, performSearch, setRecordPickerSearchFilter], + ); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/constants/MultipleRecordPickerClickOutsideListenerId.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/constants/MultipleRecordPickerClickOutsideListenerId.ts new file mode 100644 index 000000000..094cfa974 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/constants/MultipleRecordPickerClickOutsideListenerId.ts @@ -0,0 +1,2 @@ +export const MULTIPLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID = + 'multiple-record-picker-click-outside-listener'; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts new file mode 100644 index 000000000..650d21353 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts @@ -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; + 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({ + 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; + 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({ + query: combinedSearchRecordsQueryExcludingPickedRecords, + variables: { + search: searchFilter, + ...limitPerMetadataItem, + ...filterPerMetadataItemExcludingPickedRecordId, + }, + }); + + const { + recordsWithObjectMetadataId: + recordsWithObjectMetadataIdExcludingPickedRecords, + } = multipleRecordPickerformatQueryResultAsRecordsWithObjectMetadataId({ + objectMetadataItems: searchableObjectMetadataItems, + searchQueryResult: combinedSearchRecordExcludingPickedRecordsQueryResult, + }); + + return recordsWithObjectMetadataIdExcludingPickedRecords; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext.ts new file mode 100644 index 000000000..845fd9cfb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext.ts @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState.ts new file mode 100644 index 000000000..5effb3267 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerIsLoadingComponentState.ts @@ -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({ + key: 'multipleRecordPickerIsLoadingComponentState', + defaultValue: false, + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState.ts new file mode 100644 index 000000000..ffe9f8953 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState.ts @@ -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({ + key: 'multipleRecordPickerPickableMorphItemsComponentState', + defaultValue: [], + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState.ts new file mode 100644 index 000000000..d716d77a8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState.ts @@ -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({ + key: 'multipleRecordPickerSearchFilterComponentState', + defaultValue: '', + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState.ts new file mode 100644 index 000000000..23b544b1f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState.ts @@ -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({ + key: 'multipleRecordPickerSearchableObjectMetadataItemsComponentState', + defaultValue: [], + componentInstanceContext: MultipleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts new file mode 100644 index 000000000..d4bfc760e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts @@ -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({ + 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; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector.ts new file mode 100644 index 000000000..33c0b581c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableMorphItemsLengthComponentSelector.ts @@ -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; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector.ts new file mode 100644 index 000000000..4fdd384ff --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerPickableRecordIdsMatchingSearchComponentSelector.ts @@ -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); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector.ts new file mode 100644 index 000000000..955ded9f3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector.ts @@ -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; + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope.ts new file mode 100644 index 000000000..c00ad72e4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum MultipleRecordPickerHotkeyScope { + MultipleRecordPicker = 'multiple-record-picker', +} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/MultipleRecordPickerRecords.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts similarity index 85% rename from packages/twenty-front/src/modules/object-record/record-picker/types/MultipleRecordPickerRecords.ts rename to packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts index ff2c78f10..608dafdb9 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/types/MultipleRecordPickerRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts @@ -1,4 +1,4 @@ -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; export type MultipleRecordPickerRecords< CustomRecordForRecordPicker extends SingleRecordPickerRecord, diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId.ts new file mode 100644 index 000000000..8de4e4d6a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId.ts @@ -0,0 +1,5 @@ +export const getMultipleRecordPickerSelectableListId = ( + multipleRecordPickerComponentInstanceId: string, +) => { + return `${multipleRecordPickerComponentInstanceId}-selectable-list`; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multiRecordPickerFormatSearchResults.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multiRecordPickerFormatSearchResults.ts new file mode 100644 index 000000000..c69cccdeb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multiRecordPickerFormatSearchResults.ts @@ -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); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId.ts new file mode 100644 index 000000000..a1d837c4f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/utils/multipleRecordPickerformatQueryResultAsRecordWithObjectMetadataId.ts @@ -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, + }; + }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx similarity index 73% rename from packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPicker.tsx rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx index 3e9acbd6c..5235e26a2 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx @@ -3,12 +3,14 @@ import { useRef } from 'react'; import { SingleRecordPickerMenuItemsWithSearch, SingleRecordPickerMenuItemsWithSearchProps, -} from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; +} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from 'twenty-shared'; +export const SINGLE_RECORD_PICKER_LISTENER_ID = 'single-record-select'; + export type SingleRecordPickerProps = { width?: number; componentInstanceId: string; @@ -22,9 +24,9 @@ export const SingleRecordPicker = ({ onCreate, onRecordSelected, objectNameSingular, - selectedRecordIds, width = 200, componentInstanceId, + layoutDirection, }: SingleRecordPickerProps) => { const containerRef = useRef(null); @@ -41,11 +43,11 @@ export const SingleRecordPicker = ({ onCancel(); } }, - listenerId: 'single-record-select', + listenerId: SINGLE_RECORD_PICKER_LISTENER_ID, }); return ( - @@ -58,10 +60,10 @@ export const SingleRecordPicker = ({ onCreate, onRecordSelected, objectNameSingular, - selectedRecordIds, + layoutDirection, }} /> - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx similarity index 64% rename from packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItem.tsx rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx index 21ca2eeaa..5284dac47 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx @@ -2,10 +2,12 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import { Avatar, MenuItemSelectAvatar } from 'twenty-ui'; -import { RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID } from '@/object-record/record-picker/constants/RecordPickerSelectableListComponentInstanceId'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; type SingleRecordPickerMenuItemProps = { record: SingleRecordPickerRecord; @@ -22,11 +24,20 @@ export const SingleRecordPickerMenuItem = ({ onRecordSelected, selectedRecord, }: SingleRecordPickerMenuItemProps) => { + const recordPickerComponentInstanceId = + useAvailableComponentInstanceIdOrThrow( + SingleRecordPickerComponentInstanceContext, + ); + + const selectableListComponentInstanceId = + getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId); + const { isSelectedItemIdSelector } = useSelectableList( - RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID, + selectableListComponentInstanceId, ); const isSelectedItemId = useRecoilValue(isSelectedItemIdSelector(record.id)); + return ( { const containerRef = useRef(null); @@ -60,8 +67,16 @@ export const SingleRecordPickerMenuItems = ({ isDefined(entity) && isNonEmptyString(entity.name), ); + const recordPickerComponentInstanceId = + useAvailableComponentInstanceIdOrThrow( + SingleRecordPickerComponentInstanceContext, + ); + + const selectableListComponentInstanceId = + getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId); + const { isSelectedItemIdSelector, resetSelectedItem } = useSelectableList( - RECORD_PICKER_SELECTABLE_LIST_COMPONENT_INSTANCE_ID, + selectableListComponentInstanceId, ); const isSelectedSelectNoneButton = useRecoilValue( @@ -79,17 +94,21 @@ export const SingleRecordPickerMenuItems = ({ ); const selectableItemIds = recordsInDropdown.map((entity) => entity.id); + const [selectedRecordId, setSelectedRecordId] = useRecoilComponentStateV2( + singleRecordPickerSelectedIdComponentState, + ); return ( -
+ { const recordIndex = recordsInDropdown.findIndex( (record) => record.id === itemId, ); + setSelectedRecordId(itemId); onRecordSelected(recordsInDropdown[recordIndex]); resetSelectedItem(); }} @@ -107,10 +126,13 @@ export const SingleRecordPickerMenuItems = ({ emptyLabel && ( onRecordSelected()} + onClick={() => { + setSelectedRecordId(undefined); + onRecordSelected(); + }} LeftIcon={EmptyIcon} text={emptyLabel} - selected={shouldSelectEmptyOption === true} + selected={isUndefined(selectedRecordId)} hovered={isSelectedSelectNoneButton} /> ) @@ -131,6 +153,6 @@ export const SingleRecordPickerMenuItems = ({ )} -
+ ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx similarity index 53% rename from packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx index d2237ef09..c14d4ac20 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx @@ -1,11 +1,12 @@ import { SingleRecordPickerMenuItems, SingleRecordPickerMenuItemsProps, -} from '@/object-record/record-picker/components/SingleRecordPickerMenuItems'; -import { useRecordPickerRecordsOptions } from '@/object-record/record-picker/hooks/useRecordPickerRecordsOptions'; -import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState'; +} from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems'; +import { useSingleRecordPickerRecords } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords'; +import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; +import { RecordPickerLayoutDirection } from '@/object-record/record-picker/types/RecordPickerLayoutDirection'; import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission'; import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -13,18 +14,15 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { Placement } from '@floating-ui/react'; import { isDefined } from 'twenty-shared'; import { IconPlus } from 'twenty-ui'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export type SingleRecordPickerMenuItemsWithSearchProps = { excludedRecordIds?: string[]; onCreate?: ((searchInput?: string) => void) | (() => void); objectNameSingular: string; recordPickerInstanceId?: string; - selectedRecordIds: string[]; - dropdownPlacement?: Placement | null; + layoutDirection?: RecordPickerLayoutDirection; } & Pick< SingleRecordPickerMenuItemsProps, | 'EmptyIcon' @@ -42,25 +40,23 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ onCreate, onRecordSelected, objectNameSingular, - selectedRecordIds, - dropdownPlacement, + layoutDirection = 'search-bar-on-top', }: SingleRecordPickerMenuItemsWithSearchProps) => { - const { handleSearchFilterChange } = useRecordSelectSearch(); + const { handleSearchFilterChange } = useSingleRecordPickerSearch(); const recordPickerInstanceId = useAvailableComponentInstanceIdOrThrow( - RecordPickerComponentInstanceContext, + SingleRecordPickerComponentInstanceContext, ); const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission(); const recordPickerSearchFilter = useRecoilComponentValueV2( - recordPickerSearchFilterComponentState, + singleRecordPickerSearchFilterComponentState, recordPickerInstanceId, ); - const { records } = useRecordPickerRecordsOptions({ + const { records } = useSingleRecordPickerRecords({ objectNameSingular, - selectedRecordIds, excludedRecordIds, }); @@ -72,12 +68,9 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ /> ); - const shouldDisplayDropdownMenuItems = - records.recordsToSelect.length + records.selectedRecords?.length > 0; - return ( <> - {dropdownPlacement?.includes('end') && ( + {layoutDirection === 'search-bar-on-bottom' && ( <> {isDefined(onCreate) && !hasObjectReadOnlyPermission && ( @@ -85,22 +78,18 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ )} {records.recordsToSelect.length > 0 && } - {shouldDisplayDropdownMenuItems && ( - - )} + )} @@ -109,26 +98,21 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ autoFocus role="combobox" /> - {(dropdownPlacement?.includes('start') || - isUndefinedOrNull(dropdownPlacement)) && ( + {layoutDirection === 'search-bar-on-top' && ( <> - {shouldDisplayDropdownMenuItems && ( - - )} + {records.recordsToSelect.length > 0 && isDefined(onCreate) && ( )} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/components/__stories__/SingleRecordPicker.stories.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx similarity index 93% rename from packages/twenty-front/src/modules/object-record/record-picker/components/__stories__/SingleRecordPicker.stories.tsx rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx index df638799c..dda45db93 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/components/__stories__/SingleRecordPicker.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx @@ -10,8 +10,8 @@ import { graphqlMocks } from '~/testing/graphqlMocks'; import { allMockPersonRecords } from '~/testing/mock-data/people'; import { sleep } from '~/utils/sleep'; -import { SingleRecordPicker } from '@/object-record/record-picker/components/SingleRecordPicker'; -import { SingleRecordPickerRecord } from '../../types/SingleRecordPickerRecord'; +import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; const records = allMockPersonRecords.map( (person) => ({ @@ -34,7 +34,6 @@ const meta: Meta = { ], args: { objectNameSingular: CoreObjectNameSingular.WorkspaceMember, - selectedRecordIds: [], componentInstanceId: 'single-record-picker', }, argTypes: { diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/constants/SingleRecordPickerClickOutsideListenerId.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/constants/SingleRecordPickerClickOutsideListenerId.ts new file mode 100644 index 000000000..29c9ea628 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/constants/SingleRecordPickerClickOutsideListenerId.ts @@ -0,0 +1,2 @@ +export const SINGLE_RECORD_PICKER_CLICK_OUTSIDE_LISTENER_ID = + 'single-record-picker-click-outside-listener'; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/__tests__/useRecordSelectSearch.test.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/__tests__/useSingleRecordPickerRecords.test.tsx similarity index 56% rename from packages/twenty-front/src/modules/object-record/record-picker/hooks/__tests__/useRecordSelectSearch.test.tsx rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/__tests__/useSingleRecordPickerRecords.test.tsx index 1413ff599..9b7630f63 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/hooks/__tests__/useRecordSelectSearch.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/__tests__/useSingleRecordPickerRecords.test.tsx @@ -2,25 +2,25 @@ import { act, renderHook } from '@testing-library/react'; import { ChangeEvent } from 'react'; import { RecoilRoot } from 'recoil'; -import { useRecordSelectSearch } from '@/object-record/record-picker/hooks/useRecordSelectSearch'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState'; +import { useSingleRecordPickerSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const instanceId = 'instanceId'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( - + {children} - + ); -describe('useRecordSelectSearch', () => { +describe('useSingleRecordPickerRecords', () => { it('should update searchFilter after change event', async () => { const { result } = renderHook( () => { - const recordSelectSearchHook = useRecordSelectSearch(instanceId); + const recordSelectSearchHook = useSingleRecordPickerSearch(instanceId); const internallyStoredFilter = useRecoilComponentValueV2( - recordPickerSearchFilterComponentState, + singleRecordPickerSearchFilterComponentState, instanceId, ); return { recordSelectSearchHook, internallyStoredFilter }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts new file mode 100644 index 000000000..bf19e75cb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts @@ -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 }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordSelectSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts similarity index 56% rename from packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordSelectSearch.ts rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts index b5b3dbc20..2b6a421c0 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordSelectSearch.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerSearch.ts @@ -1,26 +1,26 @@ -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { recordPickerPreselectedIdComponentState } from '@/object-record/record-picker/states/recordPickerPreselectedIdComponentState'; -import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState'; +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; +import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useDebouncedCallback } from 'use-debounce'; -export const useRecordSelectSearch = ( +export const useSingleRecordPickerSearch = ( recordPickerComponentInstanceIdFromProps?: string, ) => { const recordPickerComponentInstanceId = useAvailableComponentInstanceIdOrThrow( - RecordPickerComponentInstanceContext, + SingleRecordPickerComponentInstanceContext, recordPickerComponentInstanceIdFromProps, ); const setRecordPickerSearchFilter = useSetRecoilComponentStateV2( - recordPickerSearchFilterComponentState, + singleRecordPickerSearchFilterComponentState, recordPickerComponentInstanceId, ); - const setRecordPickerPreselectedId = useSetRecoilComponentStateV2( - recordPickerPreselectedIdComponentState, + const setRecordPickerSelectedId = useSetRecoilComponentStateV2( + singleRecordPickerSelectedIdComponentState, recordPickerComponentInstanceId, ); const debouncedSetSearchFilter = useDebouncedCallback( @@ -33,14 +33,14 @@ export const useRecordSelectSearch = ( const resetSearchFilter = () => { debouncedSetSearchFilter(''); - setRecordPickerPreselectedId(''); + setRecordPickerSelectedId(undefined); }; const handleSearchFilterChange = ( event: React.ChangeEvent, ) => { debouncedSetSearchFilter(event.currentTarget.value); - setRecordPickerPreselectedId(''); + setRecordPickerSelectedId(undefined); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext.ts similarity index 73% rename from packages/twenty-front/src/modules/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext.ts rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext.ts index 919d639c0..674188169 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext.ts @@ -1,4 +1,4 @@ import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; -export const RecordPickerComponentInstanceContext = +export const SingleRecordPickerComponentInstanceContext = createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState.ts new file mode 100644 index 000000000..03d509077 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState.ts @@ -0,0 +1,9 @@ +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const singleRecordPickerSearchFilterComponentState = + createComponentStateV2({ + key: 'singleRecordPickerSearchFilterComponentState', + defaultValue: '', + componentInstanceContext: SingleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchQueryComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchQueryComponentState.ts new file mode 100644 index 000000000..dc08ed20e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchQueryComponentState.ts @@ -0,0 +1,10 @@ +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { RecordPickerSearchQuery } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerSearchQuery'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const singleRecordPickerSearchQueryComponentState = + createComponentStateV2({ + key: 'singleRecordPickerSearchQueryComponentState', + defaultValue: null, + componentInstanceContext: SingleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState.ts new file mode 100644 index 000000000..5126dcee3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState.ts @@ -0,0 +1,9 @@ +import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const singleRecordPickerSelectedIdComponentState = + createComponentStateV2({ + key: 'singleRecordPickerSelectedIdComponentState', + defaultValue: undefined, + componentInstanceContext: SingleRecordPickerComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope.ts new file mode 100644 index 000000000..2f85b0eb2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope.ts @@ -0,0 +1,3 @@ +export enum SingleRecordPickerHotkeyScope { + SingleRecordPicker = 'single-record-picker', +} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/SingleRecordPickerRecord.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-picker/types/SingleRecordPickerRecord.ts rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerSearchQuery.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerSearchQuery.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerSearchQuery.ts rename to packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerSearchQuery.ts diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId.ts new file mode 100644 index 000000000..1fbdfa804 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId.ts @@ -0,0 +1,5 @@ +export const getSingleRecordPickerSelectableListId = ( + singleRecordPickerComponentInstanceId: string, +) => { + return `${singleRecordPickerComponentInstanceId}-selectable-list`; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerPreselectedIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerPreselectedIdComponentState.ts deleted file mode 100644 index 4967b443b..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerPreselectedIdComponentState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; - -export const recordPickerPreselectedIdComponentState = createComponentStateV2< - string | undefined ->({ - key: 'recordPickerPreselectedIdComponentState', - defaultValue: undefined, - componentInstanceContext: RecordPickerComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchFilterComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchFilterComponentState.ts deleted file mode 100644 index d6fe3b983..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchFilterComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; - -export const recordPickerSearchFilterComponentState = - createComponentStateV2({ - key: 'recordPickerSearchFilterComponentState', - defaultValue: '', - componentInstanceContext: RecordPickerComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchQueryComponentState.ts b/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchQueryComponentState.ts deleted file mode 100644 index cf8d9a1b7..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/states/recordPickerSearchQueryComponentState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { RecordPickerSearchQuery } from '@/object-record/record-picker/types/RecordPickerSearchQuery'; -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; - -export const recordPickerSearchQueryComponentState = - createComponentStateV2({ - key: 'recordPickerSearchQueryComponentState', - defaultValue: null, - componentInstanceContext: RecordPickerComponentInstanceContext, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerHotkeyScope.ts b/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerHotkeyScope.ts deleted file mode 100644 index 180b37d96..000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerHotkeyScope.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum RecordPickerHotkeyScope { - RecordPicker = 'record-picker', -} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerLayoutDirection.ts b/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerLayoutDirection.ts new file mode 100644 index 000000000..096978139 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerLayoutDirection.ts @@ -0,0 +1,3 @@ +export type RecordPickerLayoutDirection = + | 'search-bar-on-top' + | 'search-bar-on-bottom'; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerPickableMorphItem.ts b/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerPickableMorphItem.ts new file mode 100644 index 000000000..b2f0cabb0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/types/RecordPickerPickableMorphItem.ts @@ -0,0 +1,6 @@ +export type RecordPickerPickableMorphItem = { + recordId: string; + objectMetadataId: string; + isSelected: boolean; + isMatchingSearchFilter: boolean; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index dc72ae32a..bccdd865f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -8,15 +8,18 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; -import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { MultipleRecordPicker } from '@/object-record/record-picker/components/MultipleRecordPicker'; -import { SingleRecordPickerMenuItemsWithSearch } from '@/object-record/record-picker/components/SingleRecordPickerMenuItemsWithSearch'; -import { RecordPickerComponentInstanceContext } from '@/object-record/record-picker/states/contexts/RecordPickerComponentInstanceContext'; -import { recordPickerSearchFilterComponentState } from '@/object-record/record-picker/states/recordPickerSearchFilterComponentState'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; +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 { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +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 { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList'; import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; @@ -78,21 +81,44 @@ export const RecordDetailRelationSection = ({ ? [fieldValue as ObjectRecord] : ((fieldValue as ObjectRecord[]) ?? []); - const relationRecordIds = relationRecords.map(({ id }) => id); - const dropdownId = `record-field-card-relation-picker-${fieldDefinition.fieldMetadataId}-${recordId}`; const { closeDropdown, isDropdownOpen, dropdownPlacement } = useDropdown(dropdownId); - const setRecordPickerSearchFilter = useSetRecoilComponentStateV2( - recordPickerSearchFilterComponentState, + const setMultipleRecordPickerSearchFilter = useSetRecoilComponentStateV2( + multipleRecordPickerSearchFilterComponentState, + dropdownId, + ); + + const setMultipleRecordPickerPickableMorphItems = + useSetRecoilComponentStateV2( + multipleRecordPickerPickableMorphItemsComponentState, + dropdownId, + ); + + const setMultipleRecordPickerSearchableObjectMetadataItems = + useSetRecoilComponentStateV2( + multipleRecordPickerSearchableObjectMetadataItemsComponentState, + dropdownId, + ); + + const { performSearch: multipleRecordPickerPerformSearch } = + useMultipleRecordPickerPerformSearch(); + + const setSingleRecordPickerSearchFilter = useSetRecoilComponentStateV2( + singleRecordPickerSearchFilterComponentState, + dropdownId, + ); + + const setSingleRecordPickerSelectedId = useSetRecoilComponentStateV2( + singleRecordPickerSelectedIdComponentState, dropdownId, ); const handleCloseRelationPickerDropdown = useCallback(() => { - setRecordPickerSearchFilter(''); - }, [setRecordPickerSearchFilter]); + setMultipleRecordPickerSearchFilter(''); + }, [setMultipleRecordPickerSearchFilter]); const persistField = usePersistField(); const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({ @@ -119,9 +145,7 @@ export const RecordDetailRelationSection = ({ }); }; - const { updateRelation } = useUpdateRelationFromManyFieldInput({ - scopeId: dropdownId, - }); + const { updateRelation } = useUpdateRelationFromManyFieldInput(); const indexViewId = useRecoilValue( prefetchIndexViewIdFromObjectMetadataItemFamilySelector({ @@ -170,6 +194,40 @@ export const RecordDetailRelationSection = ({ const relationRecordsCount = relationRecords.length; + const handleOpenRelationPickerDropdown = () => { + if (isToOneObject) { + setSingleRecordPickerSearchFilter(''); + setSingleRecordPickerSelectedId(relationRecords[0].id); + } + + if (isToManyObjects) { + setMultipleRecordPickerSearchableObjectMetadataItems([ + relationObjectMetadataItem, + ]); + setMultipleRecordPickerSearchFilter(''); + setMultipleRecordPickerPickableMorphItems( + relationRecords.map((record) => ({ + recordId: record.id, + objectMetadataId: relationObjectMetadataItem.id, + isSelected: true, + isMatchingSearchFilter: true, + })), + ); + + multipleRecordPickerPerformSearch({ + multipleRecordPickerInstanceId: dropdownId, + forceSearchFilter: '', + forceSearchableObjectMetadataItems: [relationObjectMetadataItem], + forcePickableMorphItems: relationRecords.map((record) => ({ + recordId: record.id, + objectMetadataId: relationObjectMetadataItem.id, + isSelected: true, + isMatchingSearchFilter: true, + })), + }); + } + }; + return ( } dropdownComponents={ - - {isToOneObject ? ( - - ) : ( - <> - - { - closeDropdown(); - createNewRecordAndOpenRightDrawer?.(); - }} - onChange={updateRelation} - onSubmit={closeDropdown} - dropdownPlacement={dropdownPlacement} - /> - - )} - + isToOneObject ? ( + + ) : ( + { + closeDropdown(); + createNewRecordAndOpenRightDrawer?.(); + }} + onChange={updateRelation} + onSubmit={closeDropdown} + onClickOutside={closeDropdown} + layoutDirection={ + dropdownPlacement?.includes('end') + ? 'search-bar-on-bottom' + : 'search-bar-on-top' + } + /> + ) } dropdownHotkeyScope={{ scope: dropdownId }} /> diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx index 25ce797fe..ac4aeb29a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx @@ -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 { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; -import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; -import { ReactElement } from 'react'; +import { + MiddlewareState, + autoUpdate, + flip, + offset, + useFloating, +} from '@floating-ui/react'; +import { ReactElement, useContext } from 'react'; const StyledEditableCellEditModeContainer = styled.div` align-items: center; @@ -22,15 +33,45 @@ export type RecordTableCellEditModeProps = { export const RecordTableCellEditMode = ({ children, }: RecordTableCellEditModeProps) => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + const instanceId = getRecordFieldInputId( + recordId, + fieldDefinition?.metadata?.fieldName, + ); + + const setFieldInputLayoutDirection = useSetRecoilComponentStateV2( + recordFieldInputLayoutDirectionComponentState, + instanceId, + ); + + const setFieldInputLayoutDirectionLoading = useSetRecoilComponentStateV2( + recordFieldInputLayoutDirectionLoadingComponentState, + instanceId, + ); + + const setFieldInputLayoutDirectionMiddleware = { + name: 'middleware', + fn: async (state: MiddlewareState) => { + setFieldInputLayoutDirection( + state.placement.startsWith('bottom') ? 'downward' : 'upward', + ); + setFieldInputLayoutDirectionLoading(false); + return {}; + }, + }; + const { refs, floatingStyles } = useFloating({ - placement: 'top-start', + placement: 'bottom-start', middleware: [ flip(), offset({ mainAxis: -33, crossAxis: -3, }), + setFieldInputLayoutDirectionMiddleware, ], + whileElementsMounted: autoUpdate, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index 9e1f3787c..c45457300 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -2,16 +2,20 @@ import { ReactNode, useContext } from 'react'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { RelationPickerHotkeyScope } from '@/object-record/record-field/meta-types/input/types/RelationPickerHotkeyScope'; +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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; +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 { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; +import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope'; +import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const RecordTableCellFieldContextWrapper = ({ @@ -33,11 +37,36 @@ export const RecordTableCellFieldContextWrapper = ({ return null; } - const customHotkeyScope = isFieldRelation(columnDefinition) - ? RelationPickerHotkeyScope.RelationPicker - : isFieldSelect(columnDefinition) - ? SelectFieldHotkeyScope.SelectField - : TableHotkeyScope.CellEditMode; + // TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope + const computedHotkeyScope = ( + columnDefinition: ColumnDefinition, + ) => { + 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 TableHotkeyScope.CellEditMode; + }; + + const customHotkeyScope = computedHotkeyScope(columnDefinition); return ( { const { openRecordInCommandMenu } = useCommandMenu(); + const { openFieldInput } = useOpenFieldInputEditMode(); + const openTableCell = useRecoilCallback( ({ snapshot, set }) => ({ @@ -152,6 +155,11 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { setDragSelectionStartEnabled(false); + openFieldInput({ + fieldDefinition, + recordId, + }); + moveEditModeToTableCellPosition(cellPosition); initDraftValue({ @@ -185,6 +193,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => { [ getClickOutsideListenerIsActivatedState, setDragSelectionStartEnabled, + openFieldInput, moveEditModeToTableCellPosition, initDraftValue, toggleClickOutsideListener, diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts deleted file mode 100644 index a1266fb6f..000000000 --- a/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; - -export type ObjectRecordForSelect = { - objectMetadataItem: ObjectMetadataItem; - record: ObjectRecord; - recordIdentifier: ObjectRecordIdentifier; -}; diff --git a/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts b/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts deleted file mode 100644 index 2c9bb2353..000000000 --- a/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type SelectedObjectRecordId = { - objectNameSingular: string; - id: string; -}; diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx index ce06e67cb..98eae3f7c 100644 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx @@ -7,7 +7,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { MultipleRecordPickerRecords } from '@/object-record/record-picker/types/MultipleRecordPickerRecords'; +import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { query, diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts index 5cab27401..84a98447d 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts @@ -1,14 +1,11 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; -import { MultipleRecordPickerRecords } from '@/object-record/record-picker/types/MultipleRecordPickerRecords'; -import { SingleRecordPickerRecord } from '@/object-record/record-picker/types/SingleRecordPickerRecord'; +import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords'; +import { SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from 'twenty-shared'; -// TODO: use this for all search queries, because we need selectedRecords and recordsToSelect each time we want to search -// Filtered entities to select are - export const useFilteredSearchRecordQuery = ({ selectedIds, limit, @@ -37,7 +34,7 @@ export const useFilteredSearchRecordQuery = ({ objectNameSingular, filter: selectedIdsFilter, skip: !selectedIds.length, - searchInput: searchFilter, + searchInput: '', }); const { diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 66432de58..6d81c4265 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -3,10 +3,8 @@ import { DropdownOnToggleEffect } from '@/ui/layout/dropdown/components/Dropdown import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState'; -import { dropdownMaxHeightComponentStateV2 } from '@/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; import { Placement, @@ -68,11 +66,6 @@ export const Dropdown = ({ }: DropdownProps) => { const { isDropdownOpen, toggleDropdown } = useDropdown(dropdownId); - const setDropdownMaxHeight = useSetRecoilComponentStateV2( - dropdownMaxHeightComponentStateV2, - dropdownId, - ); - const isUsingOffset = isDefined(dropdownOffset?.x) || isDefined(dropdownOffset?.y); @@ -92,9 +85,10 @@ export const Dropdown = ({ flip(), size({ padding: 32, - apply: ({ availableHeight }) => { + apply: () => { flushSync(() => { - setDropdownMaxHeight(availableHeight); + // TODO: I think this is not needed anymore let's remove it if not used for a few weeks + // setDropdownMaxHeight(availableHeight); }); }, boundary: document.querySelector('#root') ?? undefined, diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx index 4de0530e8..5d1710964 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx @@ -11,7 +11,7 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{ display: flex; flex-direction: column; - max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')}; + max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '168px' : 'none')}; padding: var(--padding); @@ -72,7 +72,6 @@ export const DropdownMenuItemsContainer = ({ { + if ( + isDefined(draftValue?.value) && + !isStandaloneVariableString(draftValue.value) + ) { + setRecordPickerSelectedId(draftValue.value); + } + }; + return ( {label ? {label} : null} @@ -143,6 +158,7 @@ export const WorkflowSingleRecordPicker = ({ dropdownId={dropdownId} dropdownPlacement="left-start" onClose={handleCloseRelationPickerDropdown} + onOpen={handleOpenDropdown} clickableComponent={ } dropdownHotkeyScope={{ scope: dropdownId }} diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx index cb1e10c1f..46b448495 100644 --- a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx @@ -50,10 +50,10 @@ const StyledOverflowingText = styled.div<{ font-weight: inherit; max-width: 100%; - overflow: hidden; text-decoration: inherit; text-overflow: ellipsis; + overflow: hidden; height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')}; white-space: nowrap;