adding object name in morph Pickers (#14977)

Morph related PR to implement this
<img width="321" height="288" alt="Screenshot 2025-10-08 at 13 32 45"
src="https://github.com/user-attachments/assets/b58e2caa-2792-43aa-8077-fa6717bfef5d"
/>


Fixes https://github.com/twentyhq/core-team-issues/issues/1308
This commit is contained in:
Guillim
2025-10-10 18:33:03 +02:00
committed by GitHub
parent f84d0514b7
commit 00eef8ba33
10 changed files with 72 additions and 5 deletions
@@ -9,6 +9,7 @@ import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-r
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useContext } from 'react';
@@ -90,6 +91,7 @@ export const MorphRelationManyToOneFieldInput = () => {
? 'search-bar-on-top'
: 'search-bar-on-bottom'
}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
);
};
@@ -5,6 +5,7 @@ import { useUpdateMorphRelationOneToManyFieldInput } from '@/object-record/recor
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -40,6 +41,7 @@ export const MorphRelationOneToManyFieldInput = () => {
? 'search-bar-on-top'
: 'search-bar-on-bottom'
}
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
);
};
@@ -10,6 +10,7 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useRelationField } from '@/object-record/record-field/ui/meta-types/hooks/useRelationField';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
@@ -22,7 +23,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput';
export const RelationOneToManyFieldInput = () => {
const { fieldDefinition, recordId } = useContext(FieldContext);
@@ -29,6 +29,7 @@ type MultipleRecordPickerProps = {
onClickOutside: () => void;
focusId: string;
objectMetadataItemIdForCreate?: string;
dropdownWidth?: number;
};
export const MultipleRecordPicker = ({
@@ -40,6 +41,7 @@ export const MultipleRecordPicker = ({
componentInstanceId,
focusId,
objectMetadataItemIdForCreate,
dropdownWidth,
}: MultipleRecordPickerProps) => {
const selectableListComponentInstanceId =
getMultipleRecordPickerSelectableListId(componentInstanceId);
@@ -129,7 +131,7 @@ export const MultipleRecordPicker = ({
containerRef={containerRef}
onClickOutside={handleClickOutside}
/>
<DropdownContent ref={containerRef}>
<DropdownContent ref={containerRef} widthInPixels={dropdownWidth}>
{layoutDirection === 'search-bar-on-bottom' && (
<>
{createNewButtonSection}
@@ -10,8 +10,12 @@ import { SelectableListItem } from '@/ui/layout/selectable-list/components/Selec
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { capitalize } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/display';
import { MenuItemMultiSelectAvatar } from 'twenty-ui/navigation';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { type SearchRecord } from '~/generated-metadata/graphql';
export const StyledSelectableItem = styled(SelectableListItem)`
@@ -62,6 +66,13 @@ export const MultipleRecordPickerMenuItemContent = ({
searchRecord.label?.trim() ||
`Untitled ${objectMetadataItem.labelSingular}`;
const searchableObjectMetadataItems = useRecoilComponentValue(
multipleRecordPickerSearchableObjectMetadataItemsComponentState,
componentInstanceId,
);
const showObjectName = searchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
itemId={searchRecord.recordId}
@@ -82,6 +93,11 @@ export const MultipleRecordPickerMenuItemContent = ({
/>
}
text={displayText}
contextualText={
showObjectName
? capitalize(searchRecord.objectNameSingular)
: undefined
}
/>
</StyledSelectableItem>
);
@@ -3,14 +3,16 @@ import styled from '@emotion/styled';
import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
import { singleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchableObjectMetadataItemsComponentState';
import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/display';
import { MenuItemSelectAvatar } from 'twenty-ui/navigation';
@@ -47,10 +49,17 @@ export const SingleRecordPickerMenuItem = ({
searchRecordStoreFamilyState(morphItem.recordId),
);
const searchableObjectMetadataItems = useRecoilComponentValue(
singleRecordPickerSearchableObjectMetadataItemsComponentState,
recordPickerComponentInstanceId,
);
if (!isDefined(searchRecord)) {
return null;
}
const showObjectName = searchableObjectMetadataItems.length > 1;
return (
<StyledSelectableItem
itemId={morphItem.recordId}
@@ -74,6 +83,11 @@ export const SingleRecordPickerMenuItem = ({
type={getAvatarType(searchRecord.objectNameSingular) ?? 'rounded'}
/>
}
contextualText={
showObjectName
? capitalize(searchRecord.objectNameSingular)
: undefined
}
/>
</StyledSelectableItem>
);
@@ -2,7 +2,10 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
import { singleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchableObjectMetadataItemsComponentState';
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilCallback } from 'recoil';
import { CustomError, isDefined } from 'twenty-shared/utils';
import { type SearchQuery } from '~/generated/graphql';
@@ -23,6 +26,12 @@ export const useSingleRecordPickerPerformSearch = ({
pickableMorphItems: RecordPickerPickableMorphItem[];
loading: boolean;
} => {
const singleRecordPickerInstanceId = useAvailableComponentInstanceIdOrThrow(
SingleRecordPickerComponentInstanceContext,
);
const { objectMetadataItems } = useObjectMetadataItems();
const onSearchRecordsCompleted = useRecoilCallback(
({ set }) =>
(data: SearchQuery) => {
@@ -34,11 +43,17 @@ export const useSingleRecordPickerPerformSearch = ({
record: undefined,
});
});
set(
singleRecordPickerSearchableObjectMetadataItemsComponentState.atomFamily(
{ instanceId: singleRecordPickerInstanceId },
),
objectMetadataItems,
);
},
[],
[objectMetadataItems, singleRecordPickerInstanceId],
);
const { objectMetadataItems } = useObjectMetadataItems();
const selectedIdsFilter = { id: { in: selectedIds } };
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
@@ -0,0 +1,10 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const singleRecordPickerSearchableObjectMetadataItemsComponentState =
createComponentState<ObjectMetadataItem[]>({
key: 'singleRecordPickerSearchableObjectMetadataItemsComponentState',
defaultValue: [],
componentInstanceContext: SingleRecordPickerComponentInstanceContext,
});
@@ -23,6 +23,9 @@ const StyledTextContainer = styled.div`
align-items: center;
flex: 1 0 0;
gap: ${({ theme }) => theme.spacing(1)};
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
`;
type MenuItemMultiSelectAvatarProps = {
@@ -29,6 +29,9 @@ const StyledTextContainer = styled.div`
align-items: center;
flex: 1 0 0;
gap: ${({ theme }) => theme.spacing(1)};
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
`;
export const MenuItemSelectAvatar = ({