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:
@ -0,0 +1,5 @@
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
export const RecordPickerNoRecordFoundMenuItem = () => {
|
||||
return <MenuItem disabled text={'No records found'} accent="placeholder" />;
|
||||
};
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -36,7 +36,7 @@ export const MenuItemDraggable = ({
|
||||
|
||||
const cursorType = showGrip
|
||||
? isDragDisabled
|
||||
? 'not-allowed'
|
||||
? 'default'
|
||||
: 'drag'
|
||||
: 'default';
|
||||
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user