fix glitch for relation picker search (#8040)

Fix for #7957

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Sanskar Jain 2024-10-25 20:21:52 +05:30 committed by GitHub
parent f633f0d330
commit 9c923ba8d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 162 additions and 92 deletions

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -1160,6 +1160,7 @@ export type UpdateObjectPayload = {
labelSingular?: InputMaybe<Scalars['String']>;
namePlural?: InputMaybe<Scalars['String']>;
nameSingular?: InputMaybe<Scalars['String']>;
shortcut?: InputMaybe<Scalars['String']>;
shouldSyncLabelAndName?: InputMaybe<Scalars['Boolean']>;
};
@ -1477,6 +1478,7 @@ export type Object = {
labelSingular: Scalars['String'];
namePlural: Scalars['String'];
nameSingular: Scalars['String'];
shortcut?: Maybe<Scalars['String']>;
shouldSyncLabelAndName: Scalars['Boolean'];
updatedAt: Scalars['DateTime'];
};

View File

@ -31,7 +31,7 @@ const StyledContainer = styled.div`
const StyledColumnContainer = styled.div`
display: flex;
& > *:not(:first-child) {
& > *:not(:first-of-type) {
border-left: 1px solid ${({ theme }) => theme.border.color.light};
}
`;

View File

@ -18,7 +18,7 @@ const StyledHeaderContainer = styled.div`
top: 0;
}
& > *:not(:first-child) {
& > *:not(:first-of-type) {
border-left: 1px solid ${({ theme }) => theme.border.color.light};
}
`;

View File

@ -10,6 +10,7 @@ import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldM
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
type RelationFromManyFieldInputProps = {
onSubmit?: FieldInputEvent;
@ -50,6 +51,8 @@ export const RelationFromManyFieldInput = ({
recordId,
});
const { dropdownPlacement } = useDropdown(relationPickerScopeId);
return (
<>
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
@ -58,6 +61,7 @@ export const RelationFromManyFieldInput = ({
onSubmit={handleSubmit}
onChange={updateRelation}
onCreate={createNewRecordAndOpenRightDrawer}
dropdownPlacement={dropdownPlacement}
/>
</RelationPickerScope>
</>

View File

@ -82,7 +82,8 @@ export const RecordDetailRelationSection = ({
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${recordId}`;
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId);
const { closeDropdown, isDropdownOpen, dropdownPlacement } =
useDropdown(dropdownId);
const { setRelationPickerSearchFilter } = useRelationPicker({
relationPickerScopeId: dropdownId,
@ -183,7 +184,7 @@ export const RecordDetailRelationSection = ({
<DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
dropdownPlacement="left-start"
onClose={handleCloseRelationPickerDropdown}
clickableComponent={
<LightIconButton
@ -204,6 +205,7 @@ export const RecordDetailRelationSection = ({
}
relationPickerScopeId={dropdownId}
onCreate={createNewRecordAndOpenRightDrawer}
dropdownPlacement={dropdownPlacement}
/>
) : (
<>
@ -212,6 +214,7 @@ export const RecordDetailRelationSection = ({
onCreate={createNewRecordAndOpenRightDrawer}
onChange={updateRelation}
onSubmit={closeDropdown}
dropdownPlacement={dropdownPlacement}
/>
</>
)}

View File

@ -5,6 +5,7 @@ import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/r
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
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';
@ -18,11 +19,12 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import styled from '@emotion/styled';
import { Placement } from '@floating-ui/react';
import { useCallback, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconPlus, isDefined } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const StyledSelectableItem = styled(SelectableItem)`
height: 100%;
@ -33,10 +35,12 @@ export const MultiRecordSelect = ({
onChange,
onSubmit,
onCreate,
dropdownPlacement,
}: {
onChange?: (changedRecordForSelectId: string) => void;
onSubmit?: () => void;
onCreate?: ((searchInput?: string) => void) | (() => void);
dropdownPlacement?: Placement | null;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const setHotkeyScope = useSetHotkeyScope();
@ -55,6 +59,7 @@ export const MultiRecordSelect = ({
const recordMultiSelectIsLoading = useRecoilValue(
recordMultiSelectIsLoadingState,
);
const objectRecordsIdsMultiSelect = useRecoilValue(
objectRecordsIdsMultiSelectState,
);
@ -67,9 +72,6 @@ export const MultiRecordSelect = ({
const relationPickerSearchFilter = useRecoilValue(
relationPickerSearchFilterState,
);
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
leading: true,
});
useEffect(() => {
setHotkeyScope(relationPickerScopedId);
@ -86,16 +88,53 @@ export const MultiRecordSelect = ({
[onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem],
);
const debouncedOnCreate = useDebouncedCallback(
() => onCreate?.(relationPickerSearchFilter),
500,
);
const handleFilterChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchFilter(event.currentTarget.value);
setSearchFilter(event.currentTarget.value);
},
[debouncedSetSearchFilter],
[setSearchFilter],
);
const results = (
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={objectRecordsIdsMultiSelect}
hotkeyScope={relationPickerScopedId}
onEnter={(selectedId) => {
onChange?.(selectedId);
resetSelectedItem();
}}
>
{objectRecordsIdsMultiSelect?.map((recordId) => {
return (
<MultipleObjectRecordSelectItem
key={recordId}
objectRecordId={recordId}
onChange={(recordId) => {
onChange?.(recordId);
resetSelectedItem();
}}
/>
);
})}
</SelectableList>
{objectRecordsIdsMultiSelect?.length === 0 &&
!recordMultiSelectIsLoading && <MenuItem text="No result" />}
</DropdownMenuItemsContainer>
);
const createNewButton = isDefined(onCreate) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<CreateNewButton
onClick={() => onCreate?.(relationPickerSearchFilter)}
LeftIcon={IconPlus}
text="Add New"
/>
</DropdownMenuItemsContainer>
</>
);
return (
@ -107,55 +146,30 @@ export const MultiRecordSelect = ({
}}
/>
<DropdownMenu ref={containerRef} data-select-disable>
{dropdownPlacement?.includes('end') && (
<>
{createNewButton}
{results}
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
<DropdownMenuSkeletonItem />
)}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSearchInput
value={relationPickerSearchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight hasMinHeight>
{recordMultiSelectIsLoading ? (
<MenuItem text="Loading..." />
) : (
<>
<SelectableList
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
selectableItemIdArray={objectRecordsIdsMultiSelect}
hotkeyScope={relationPickerScopedId}
onEnter={(selectedId) => {
onChange?.(selectedId);
resetSelectedItem();
}}
>
{objectRecordsIdsMultiSelect?.map((recordId) => {
return (
<MultipleObjectRecordSelectItem
key={recordId}
objectRecordId={recordId}
onChange={(recordId) => {
onChange?.(recordId);
resetSelectedItem();
}}
/>
);
})}
</SelectableList>
{objectRecordsIdsMultiSelect?.length === 0 && (
<MenuItem text="No result" />
)}
</>
)}
</DropdownMenuItemsContainer>
{isDefined(onCreate) && (
{(dropdownPlacement?.includes('start') ||
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<CreateNewButton
onClick={debouncedOnCreate}
LeftIcon={IconPlus}
text="Add New"
/>
</DropdownMenuItemsContainer>
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
<DropdownMenuSkeletonItem />
)}
{results}
{createNewButton}
</>
)}
</DropdownMenu>

View File

@ -36,6 +36,7 @@ export type SingleEntitySelectMenuItemsProps = {
isAllEntitySelectShown?: boolean;
onAllEntitySelected?: () => void;
hotkeyScope?: string;
isFiltered: boolean;
};
export const SingleEntitySelectMenuItems = ({
@ -54,6 +55,7 @@ export const SingleEntitySelectMenuItems = ({
isAllEntitySelectShown,
onAllEntitySelected,
hotkeyScope = RelationPickerHotkeyScope.RelationPicker,
isFiltered,
}: SingleEntitySelectMenuItemsProps) => {
const containerRef = useRef<HTMLDivElement>(null);
@ -139,9 +141,11 @@ export const SingleEntitySelectMenuItems = ({
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{loading ? (
{loading && !isFiltered ? (
<DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
) : entitiesInDropdown.length === 0 &&
!isAllEntitySelectShown &&
!loading ? (
<>
<MenuItem text="No result" />
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}

View File

@ -6,7 +6,9 @@ import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/use
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { Placement } from '@floating-ui/react';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export type SingleEntitySelectMenuItemsWithSearchProps = {
excludedRelationRecordIds?: string[];
@ -14,6 +16,7 @@ export type SingleEntitySelectMenuItemsWithSearchProps = {
relationObjectNameSingular: string;
relationPickerScopeId?: string;
selectedRelationRecordIds: string[];
dropdownPlacement?: Placement | null;
} & Pick<
SingleEntitySelectMenuItemsProps,
| 'EmptyIcon'
@ -34,6 +37,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
relationPickerScopeId = 'relation-picker',
selectedEntity,
selectedRelationRecordIds,
dropdownPlacement,
}: SingleEntitySelectMenuItemsWithSearchProps) => {
const { handleSearchFilterChange } = useEntitySelectSearch({
relationPickerScopeId,
@ -62,29 +66,45 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
};
}
const results = (
<SingleEntitySelectMenuItems
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
selectedEntity={
selectedEntity ??
(entities.selectedEntities.length === 1
? entities.selectedEntities[0]
: undefined)
}
hotkeyScope={relationPickerScopeId}
onCreate={onCreateWithInput}
isFiltered={!!relationPickerSearchFilter}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onEntitySelected,
showCreateButton,
}}
/>
);
return (
<>
{dropdownPlacement?.includes('end') && (
<>
{results}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
<DropdownMenuSeparator />
<SingleEntitySelectMenuItems
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
selectedEntity={
selectedEntity ??
(entities.selectedEntities.length === 1
? entities.selectedEntities[0]
: undefined)
}
hotkeyScope={relationPickerScopeId}
onCreate={onCreateWithInput}
{...{
EmptyIcon,
emptyLabel,
onCancel,
onEntitySelected,
showCreateButton,
}}
/>
{(dropdownPlacement?.includes('start') ||
isUndefinedOrNull(dropdownPlacement)) && (
<>
<DropdownMenuSeparator />
{results}
</>
)}
</>
);
};

View File

@ -6,7 +6,7 @@ import {
Placement,
useFloating,
} from '@floating-ui/react';
import { MouseEvent, useRef } from 'react';
import { MouseEvent, useEffect, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { Key } from 'ts-key-enum';
@ -64,8 +64,13 @@ export const Dropdown = ({
}: DropdownProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth } =
useDropdown(dropdownId);
const {
isDropdownOpen,
toggleDropdown,
closeDropdown,
dropdownWidth,
setDropdownPlacement,
} = useDropdown(dropdownId);
const offsetMiddlewares = [];
@ -77,13 +82,17 @@ export const Dropdown = ({
offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y }));
}
const { refs, floatingStyles } = useFloating({
const { refs, floatingStyles, placement } = useFloating({
placement: dropdownPlacement,
middleware: [flip(), ...offsetMiddlewares],
whileElementsMounted: autoUpdate,
strategy: dropdownStrategy,
});
useEffect(() => {
setDropdownPlacement(placement);
}, [placement, setDropdownPlacement]);
const handleHotkeyTriggered = () => {
toggleDropdown();
};

View File

@ -3,7 +3,6 @@ import styled from '@emotion/styled';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
const StyledDropdownMenuItemsExternalContainer = styled.div<{
hasMinHeight?: boolean;
hasMaxHeight?: boolean;
}>`
--padding: ${({ theme }) => theme.spacing(1)};
@ -13,7 +12,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
flex-direction: column;
gap: 2px;
min-height: ${({ hasMinHeight }) => (hasMinHeight ? '150px' : '100%')};
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')};
overflow-y: auto;
@ -38,18 +36,13 @@ const StyledDropdownMenuItemsInternalContainer = styled.div`
export const DropdownMenuItemsContainer = ({
children,
hasMinHeight,
hasMaxHeight,
}: {
children: React.ReactNode;
hasMinHeight?: boolean;
hasMaxHeight?: boolean;
}) => {
return (
<StyledDropdownMenuItemsExternalContainer
hasMaxHeight={hasMaxHeight}
hasMinHeight={hasMinHeight}
>
<StyledDropdownMenuItemsExternalContainer hasMaxHeight={hasMaxHeight}>
{hasMaxHeight ? (
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<StyledDropdownMenuItemsInternalContainer>

View File

@ -1,5 +1,6 @@
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState';
import { dropdownWidthComponentState } from '@/ui/layout/dropdown/states/dropdownWidthComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
@ -19,6 +20,10 @@ export const useDropdownStates = ({
return {
scopeId,
dropdownPlacementState: extractComponentState(
dropdownPlacementComponentState,
scopeId,
),
dropdownHotkeyScopeState: extractComponentState(
dropdownHotkeyComponentState,
scopeId,

View File

@ -12,6 +12,7 @@ export const useDropdown = (dropdownId?: string) => {
dropdownHotkeyScopeState,
dropdownWidthState,
isDropdownOpenState,
dropdownPlacementState,
} = useDropdownStates({
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
});
@ -25,6 +26,10 @@ export const useDropdown = (dropdownId?: string) => {
const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState);
const [dropdownPlacement, setDropdownPlacement] = useRecoilState(
dropdownPlacementState,
);
const [isDropdownOpen, setIsDropdownOpen] =
useRecoilState(isDropdownOpenState);
@ -59,5 +64,7 @@ export const useDropdown = (dropdownId?: string) => {
openDropdown,
dropdownWidth,
setDropdownWidth,
dropdownPlacement,
setDropdownPlacement,
};
};

View File

@ -0,0 +1,9 @@
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { Placement } from '@floating-ui/react';
export const dropdownPlacementComponentState =
createComponentState<Placement | null>({
key: 'dropdownPlacementComponentState',
defaultValue: null,
});