diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx index b6e69cc7b..9d0920149 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityTargetChips.tsx @@ -18,6 +18,7 @@ export const ActivityTargetChips = ({ {activityTargetObjectRecords?.map((activityTargetObjectRecord) => ( } - isDisplayModeContentEmpty={ - activity?.activityTargets?.edges?.length === 0 - } + isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0} /> ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx index b05854601..d79dbc3bf 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'; import { ObjectFilterDropdownId } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect'; import { SingleEntitySelectMenuItems } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems'; +import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleEntitySelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleEntitySelect.tsx deleted file mode 100644 index 3ec2edafb..000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleEntitySelect.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useRef } from 'react'; -import { isNonEmptyString } from '@sniptt/guards'; -import debounce from 'lodash.debounce'; - -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { Avatar } from '@/users/components/Avatar'; - -import { EntityForSelect } from '../types/EntityForSelect'; - -export type EntitiesForMultipleEntitySelect< - CustomEntityForSelect extends EntityForSelect, -> = { - selectedEntities: CustomEntityForSelect[]; - filteredSelectedEntities: CustomEntityForSelect[]; - entitiesToSelect: CustomEntityForSelect[]; - loading: boolean; -}; - -export const MultipleEntitySelect = < - CustomEntityForSelect extends EntityForSelect, ->({ - entities, - onChange, - onSubmit, - onSearchFilterChange, - searchFilter, - value, -}: { - entities: EntitiesForMultipleEntitySelect; - searchFilter: string; - onSearchFilterChange: (newSearchFilter: string) => void; - onChange: (value: Record) => void; - onCancel?: () => void; - onSubmit?: () => void; - value: Record; -}) => { - const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, { - leading: true, - }); - - const handleFilterChange = (event: React.ChangeEvent) => { - debouncedSetSearchFilter(event.currentTarget.value); - onSearchFilterChange(event.currentTarget.value); - }; - - let entitiesInDropdown = [ - ...(entities.filteredSelectedEntities ?? []), - ...(entities.entitiesToSelect ?? []), - ]; - - entitiesInDropdown = entitiesInDropdown.filter((entity) => - isNonEmptyString(entity.name), - ); - - const containerRef = useRef(null); - - useListenClickOutside({ - refs: [containerRef], - callback: (event) => { - event.stopImmediatePropagation(); - event.stopPropagation(); - event.preventDefault(); - - onSubmit?.(); - }, - }); - - const selectableItemIds = entitiesInDropdown.map((entity) => entity.id); - - return ( - - - - - { - if (_itemId in value === false || value[_itemId] === false) { - onChange({ ...value, [_itemId]: true }); - } else { - onChange({ ...value, [_itemId]: false }); - } - }} - > - {entitiesInDropdown?.map((entity) => ( - - - onChange({ ...value, [entity.id]: newCheckedValue }) - } - avatar={ - - } - text={entity.name} - /> - - ))} - - {entitiesInDropdown?.length === 0 && } - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx new file mode 100644 index 000000000..ee8652fb3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect.tsx @@ -0,0 +1,22 @@ +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; + +export const MultipleObjectRecordOnClickOutsideEffect = ({ + containerRef, + onClickOutside, +}: { + containerRef: React.RefObject; + onClickOutside: () => void; +}) => { + useListenClickOutside({ + refs: [containerRef], + callback: (event) => { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + + onClickOutside(); + }, + }); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx index 03cb9107b..97acf3362 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx @@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import debounce from 'lodash.debounce'; -import { v4 } from 'uuid'; +import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect'; +import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem'; +import { MultiObjectRecordSelectSelectableListId } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; import { ObjectRecordForSelect, SelectedObjectRecordId, @@ -17,9 +19,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { Avatar } from '@/users/components/Avatar'; export const StyledSelectableItem = styled(SelectableItem)` height: 100%; @@ -116,61 +115,61 @@ export const MultipleObjectRecordSelect = ({ [filteredSelectedObjectRecords, objectRecordsToSelect], ); - useListenClickOutside({ - refs: [containerRef], - callback: (event) => { - event.stopImmediatePropagation(); - event.stopPropagation(); - event.preventDefault(); - - onSubmit?.(internalSelectedRecords); - }, - }); - const selectableItemIds = entitiesInDropdown.map( (entity) => entity.record.id, ); return ( - - + { + onSubmit?.(internalSelectedRecords); + }} /> - - - {loading ? ( - - ) : ( - <> - { - const recordIsSelected = internalSelectedRecords?.some( - (selectedRecord) => selectedRecord.record.id === recordId, - ); - - const correspondingRecordForSelect = entitiesInDropdown?.find( - (entity) => entity.record.id === recordId, - ); - - if (correspondingRecordForSelect) { - handleSelectChange( - correspondingRecordForSelect, - !recordIsSelected, + + + + + {loading ? ( + + ) : ( + <> + { + const recordIsSelected = internalSelectedRecords?.some( + (selectedRecord) => selectedRecord.record.id === recordId, ); - } - }} - > - {entitiesInDropdown?.map((objectRecordForSelect) => ( - - entity.record.id === recordId, + ); + + if (correspondingRecordForSelect) { + handleSelectChange( + correspondingRecordForSelect, + !recordIsSelected, + ); + } + }} + > + {entitiesInDropdown?.map((objectRecordForSelect) => ( + + handleSelectChange( + objectRecordForSelect, + newSelectedValue, + ) + } selected={internalSelectedRecords?.some( (selectedRecord) => { return ( @@ -179,34 +178,16 @@ export const MultipleObjectRecordSelect = ({ ); }, )} - onSelectChange={(newCheckedValue) => - handleSelectChange(objectRecordForSelect, newCheckedValue) - } - avatar={ - - } - text={objectRecordForSelect.recordIdentifier.name} /> - - ))} - - {entitiesInDropdown?.length === 0 && } - - )} - - + ))} + + {entitiesInDropdown?.length === 0 && ( + + )} + + )} + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx new file mode 100644 index 000000000..e798f78ea --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelectItem.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; +import { v4 } from 'uuid'; + +import { MultiObjectRecordSelectSelectableListId } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; +import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; +import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; +import { Avatar } from '@/users/components/Avatar'; + +export const StyledSelectableItem = styled(SelectableItem)` + height: 100%; + width: 100%; +`; + +export const MultipleObjectRecordSelectItem = ({ + objectRecordForSelect, + onSelectedChange, + selected, +}: { + objectRecordForSelect: ObjectRecordForSelect; + onSelectedChange?: (selected: boolean) => void; + selected: boolean; +}) => { + const { isSelectedItemIdSelector } = useSelectableList( + MultiObjectRecordSelectSelectableListId, + ); + + const isSelectedByKeyboard = useRecoilValue( + isSelectedItemIdSelector(objectRecordForSelect.record.id), + ); + + return ( + + + } + text={objectRecordForSelect.recordIdentifier.name} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId.ts b/packages/twenty-front/src/modules/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId.ts new file mode 100644 index 000000000..7d1624e92 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId.ts @@ -0,0 +1,2 @@ +export const MultiObjectRecordSelectSelectableListId = + 'multi-object-record-select-selectable-list'; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/types/EntitiesForMultipleEntitySelect.ts b/packages/twenty-front/src/modules/object-record/relation-picker/types/EntitiesForMultipleEntitySelect.ts new file mode 100644 index 000000000..fe74c29d4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/types/EntitiesForMultipleEntitySelect.ts @@ -0,0 +1,10 @@ +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; + +export type EntitiesForMultipleEntitySelect< + CustomEntityForSelect extends EntityForSelect, +> = { + selectedEntities: CustomEntityForSelect[]; + filteredSelectedEntities: CustomEntityForSelect[]; + entitiesToSelect: CustomEntityForSelect[]; + loading: boolean; +}; diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts index e1095871e..b27492123 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts @@ -2,7 +2,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { OrderBy } from '@/object-metadata/types/OrderBy'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/components/MultipleEntitySelect'; +import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { assertNotNull } from '~/utils/assert'; import { isDefined } from '~/utils/isDefined';