Fix "No results found" in record pickers (#12685)

This PR fixes small bugs around the "No results found" record picker.

Before : 


https://github.com/user-attachments/assets/e2eee648-1cb1-40fb-ad3c-fe4724f7314e

After : 


https://github.com/user-attachments/assets/714e6dea-3c65-4e04-9fef-ee718c94bbba

Minor improvements : 
- Created a new component `RecordPickerNoRecordFoundMenuItem` to
abstract this "No records found" menu item
- Removed `not-allowed` cursor from MenuItem in disabled state.

Fixes https://github.com/twentyhq/twenty/issues/12666
This commit is contained in:
Lucas Bordeau
2025-06-17 16:10:44 +02:00
committed by GitHub
parent c88495b545
commit cc7a37b0cc
6 changed files with 106 additions and 112 deletions

View File

@ -0,0 +1,5 @@
import { MenuItem } from 'twenty-ui/navigation';
export const RecordPickerNoRecordFoundMenuItem = () => {
return <MenuItem disabled text={'No records found'} accent="placeholder" />;
};

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem';
import { MultipleRecordPickerFetchMoreLoader } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerFetchMoreLoader';
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';
@ -22,14 +23,6 @@ export const StyledSelectableItem = styled(SelectableListItem)`
width: 100%;
`;
const StyledEmptyText = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
`;
type MultipleRecordPickerMenuItemsProps = {
onChange?: (morphItem: RecordPickerPickableMorphItem) => void;
};
@ -87,7 +80,7 @@ export const MultipleRecordPickerMenuItems = ({
return (
<DropdownMenuItemsContainer hasMaxHeight>
{pickableRecordIds.length === 0 ? (
<StyledEmptyText>No results found</StyledEmptyText>
<RecordPickerNoRecordFoundMenuItem />
) : (
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}

View File

@ -1,9 +1,7 @@
import { isNonEmptyString, isUndefined } from '@sniptt/guards';
import { useRef } from 'react';
import { Key } from 'ts-key-enum';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -19,7 +17,6 @@ import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { IconComponent } from 'twenty-ui/display';
import { MenuItemSelect } from 'twenty-ui/navigation';
@ -33,13 +30,8 @@ export type SingleRecordPickerMenuItemsProps = {
onRecordSelected: (entity?: SingleRecordPickerRecord) => void;
selectedRecord?: SingleRecordPickerRecord;
hotkeyScope?: string;
isFiltered: boolean;
};
const StyledContainer = styled.div`
display: flex;
`;
export const SingleRecordPickerMenuItems = ({
EmptyIcon,
emptyLabel,
@ -49,10 +41,7 @@ export const SingleRecordPickerMenuItems = ({
onRecordSelected,
selectedRecord,
hotkeyScope = SingleRecordPickerHotkeyScope.SingleRecordPicker,
isFiltered,
}: SingleRecordPickerMenuItemsProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const selectNone = emptyLabel
? {
__typename: '',
@ -104,60 +93,54 @@ export const SingleRecordPickerMenuItems = ({
);
return (
<StyledContainer ref={containerRef}>
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
>
<DropdownMenuItemsContainer hasMaxHeight>
{loading && !isFiltered ? (
<DropdownMenuSkeletonItem />
) : recordsInDropdown.length === 0 && !loading ? (
<></>
) : (
recordsInDropdown?.map((record) => {
switch (record.id) {
case 'select-none': {
return (
emptyLabel && (
<SelectableListItem
key={record.id}
itemId={record.id}
onEnter={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
>
<MenuItemSelect
onClick={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
LeftIcon={EmptyIcon}
text={emptyLabel}
selected={isUndefined(selectedRecordId)}
focused={isSelectedSelectNoneButton}
/>
</SelectableListItem>
)
);
}
default: {
return (
<SingleRecordPickerMenuItem
key={record.id}
record={record}
onRecordSelected={onRecordSelected}
selectedRecord={selectedRecord}
<SelectableList
selectableListInstanceId={selectableListComponentInstanceId}
selectableItemIdArray={selectableItemIds}
hotkeyScope={hotkeyScope}
>
{loading ? (
<DropdownMenuSkeletonItem />
) : (
recordsInDropdown?.map((record) => {
switch (record.id) {
case 'select-none': {
return (
emptyLabel && (
<SelectableListItem
key={record.id}
itemId={record.id}
onEnter={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
>
<MenuItemSelect
onClick={() => {
setSelectedRecordId(undefined);
onRecordSelected();
}}
LeftIcon={EmptyIcon}
text={emptyLabel}
selected={isUndefined(selectedRecordId)}
focused={isSelectedSelectNoneButton}
/>
);
}
}
})
)}
</DropdownMenuItemsContainer>
</SelectableList>
</StyledContainer>
</SelectableListItem>
)
);
}
default: {
return (
<SingleRecordPickerMenuItem
key={record.id}
record={record}
onRecordSelected={onRecordSelected}
selectedRecord={selectedRecord}
/>
);
}
}
})
)}
</SelectableList>
);
};

View File

@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordPickerNoRecordFoundMenuItem } from '@/object-record/record-picker/components/RecordPickerNoRecordFoundMenuItem';
import {
SingleRecordPickerMenuItems,
SingleRecordPickerMenuItemsProps,
@ -15,6 +16,7 @@ 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 { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { IconPlus } from 'twenty-ui/display';
@ -69,13 +71,14 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
const createNewButton = isDefined(onCreate) && (
<CreateNewButton
onClick={() => onCreate?.(recordPickerSearchFilter)}
LeftIcon={IconPlus}
text="Add New"
/>
);
const searchHasNoResults =
isNonEmptyString(recordPickerSearchFilter) &&
records.recordsToSelect.length === 0 &&
!records.loading;
const handleCreateNew = () => {
onCreate?.(recordPickerSearchFilter);
};
return (
<>
@ -84,23 +87,29 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
{isDefined(onCreate) && hasObjectUpdatePermissions && (
<>
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
<CreateNewButton
onClick={handleCreateNew}
LeftIcon={IconPlus}
text="Add New"
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
<SingleRecordPickerMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}
isFiltered={!!recordPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
<DropdownMenuItemsContainer hasMaxHeight>
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
<SingleRecordPickerMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
@ -112,23 +121,29 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
{layoutDirection === 'search-bar-on-top' && (
<>
<DropdownMenuSeparator />
<SingleRecordPickerMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}
isFiltered={!!recordPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
<DropdownMenuItemsContainer hasMaxHeight>
<SingleRecordPickerMenuItems
recordsToSelect={records.recordsToSelect}
loading={records.loading}
selectedRecord={records.selectedRecords?.[0]}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onRecordSelected,
}}
/>
{searchHasNoResults && <RecordPickerNoRecordFoundMenuItem />}
</DropdownMenuItemsContainer>
{isDefined(onCreate) && hasObjectUpdatePermissions && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer scrollable={false}>
{createNewButton}
<CreateNewButton
onClick={handleCreateNew}
LeftIcon={IconPlus}
text="Add New"
/>
</DropdownMenuItemsContainer>
</>
)}

View File

@ -36,7 +36,7 @@ export const MenuItemDraggable = ({
const cursorType = showGrip
? isDragDisabled
? 'not-allowed'
? 'default'
: 'drag'
: 'default';

View File

@ -131,7 +131,7 @@ export const StyledDraggableItem = styled.div`
export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
disabled?: boolean;
isIconDisplayedOnHoverOnly?: boolean;
cursor?: 'drag' | 'default' | 'not-allowed';
cursor?: 'drag' | 'default';
}>`
${({ isIconDisplayedOnHoverOnly, theme }) =>
isIconDisplayedOnHoverOnly &&
@ -156,14 +156,12 @@ export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{
cursor: ${({ cursor, disabled }) => {
if (!isUndefined(disabled) && disabled !== false) {
return 'not-allowed';
return 'default';
}
switch (cursor) {
case 'drag':
return 'grab';
case 'not-allowed':
return 'not-allowed';
default:
return 'pointer';
}