mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 13:17:57 +00:00
fix glitch for relation picker search (#8040)
Fix for #7957 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
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']>;
|
labelSingular?: InputMaybe<Scalars['String']>;
|
||||||
namePlural?: InputMaybe<Scalars['String']>;
|
namePlural?: InputMaybe<Scalars['String']>;
|
||||||
nameSingular?: InputMaybe<Scalars['String']>;
|
nameSingular?: InputMaybe<Scalars['String']>;
|
||||||
|
shortcut?: InputMaybe<Scalars['String']>;
|
||||||
shouldSyncLabelAndName?: InputMaybe<Scalars['Boolean']>;
|
shouldSyncLabelAndName?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1477,6 +1478,7 @@ export type Object = {
|
|||||||
labelSingular: Scalars['String'];
|
labelSingular: Scalars['String'];
|
||||||
namePlural: Scalars['String'];
|
namePlural: Scalars['String'];
|
||||||
nameSingular: Scalars['String'];
|
nameSingular: Scalars['String'];
|
||||||
|
shortcut?: Maybe<Scalars['String']>;
|
||||||
shouldSyncLabelAndName: Scalars['Boolean'];
|
shouldSyncLabelAndName: Scalars['Boolean'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
const StyledColumnContainer = styled.div`
|
const StyledColumnContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
& > *:not(:first-child) {
|
& > *:not(:first-of-type) {
|
||||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const StyledHeaderContainer = styled.div`
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > *:not(:first-child) {
|
& > *:not(:first-of-type) {
|
||||||
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
border-left: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldM
|
|||||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
|
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/relation-picker/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
|
||||||
type RelationFromManyFieldInputProps = {
|
type RelationFromManyFieldInputProps = {
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
@@ -50,6 +51,8 @@ export const RelationFromManyFieldInput = ({
|
|||||||
recordId,
|
recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { dropdownPlacement } = useDropdown(relationPickerScopeId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||||
@@ -58,6 +61,7 @@ export const RelationFromManyFieldInput = ({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onChange={updateRelation}
|
onChange={updateRelation}
|
||||||
onCreate={createNewRecordAndOpenRightDrawer}
|
onCreate={createNewRecordAndOpenRightDrawer}
|
||||||
|
dropdownPlacement={dropdownPlacement}
|
||||||
/>
|
/>
|
||||||
</RelationPickerScope>
|
</RelationPickerScope>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export const RecordDetailRelationSection = ({
|
|||||||
|
|
||||||
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${recordId}`;
|
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}-${recordId}`;
|
||||||
|
|
||||||
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownId);
|
const { closeDropdown, isDropdownOpen, dropdownPlacement } =
|
||||||
|
useDropdown(dropdownId);
|
||||||
|
|
||||||
const { setRelationPickerSearchFilter } = useRelationPicker({
|
const { setRelationPickerSearchFilter } = useRelationPicker({
|
||||||
relationPickerScopeId: dropdownId,
|
relationPickerScopeId: dropdownId,
|
||||||
@@ -183,7 +184,7 @@ export const RecordDetailRelationSection = ({
|
|||||||
<DropdownScope dropdownScopeId={dropdownId}>
|
<DropdownScope dropdownScopeId={dropdownId}>
|
||||||
<StyledAddDropdown
|
<StyledAddDropdown
|
||||||
dropdownId={dropdownId}
|
dropdownId={dropdownId}
|
||||||
dropdownPlacement="right-start"
|
dropdownPlacement="left-start"
|
||||||
onClose={handleCloseRelationPickerDropdown}
|
onClose={handleCloseRelationPickerDropdown}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
<LightIconButton
|
<LightIconButton
|
||||||
@@ -204,6 +205,7 @@ export const RecordDetailRelationSection = ({
|
|||||||
}
|
}
|
||||||
relationPickerScopeId={dropdownId}
|
relationPickerScopeId={dropdownId}
|
||||||
onCreate={createNewRecordAndOpenRightDrawer}
|
onCreate={createNewRecordAndOpenRightDrawer}
|
||||||
|
dropdownPlacement={dropdownPlacement}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -212,6 +214,7 @@ export const RecordDetailRelationSection = ({
|
|||||||
onCreate={createNewRecordAndOpenRightDrawer}
|
onCreate={createNewRecordAndOpenRightDrawer}
|
||||||
onChange={updateRelation}
|
onChange={updateRelation}
|
||||||
onSubmit={closeDropdown}
|
onSubmit={closeDropdown}
|
||||||
|
dropdownPlacement={dropdownPlacement}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
import { CreateNewButton } from '@/ui/input/relation-picker/components/CreateNewButton';
|
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 { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { Placement } from '@floating-ui/react';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import { IconPlus, isDefined } from 'twenty-ui';
|
import { IconPlus, isDefined } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableItem)`
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -33,10 +35,12 @@ export const MultiRecordSelect = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
dropdownPlacement,
|
||||||
}: {
|
}: {
|
||||||
onChange?: (changedRecordForSelectId: string) => void;
|
onChange?: (changedRecordForSelectId: string) => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||||
|
dropdownPlacement?: Placement | null;
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
@@ -55,6 +59,7 @@ export const MultiRecordSelect = ({
|
|||||||
const recordMultiSelectIsLoading = useRecoilValue(
|
const recordMultiSelectIsLoading = useRecoilValue(
|
||||||
recordMultiSelectIsLoadingState,
|
recordMultiSelectIsLoadingState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const objectRecordsIdsMultiSelect = useRecoilValue(
|
const objectRecordsIdsMultiSelect = useRecoilValue(
|
||||||
objectRecordsIdsMultiSelectState,
|
objectRecordsIdsMultiSelectState,
|
||||||
);
|
);
|
||||||
@@ -67,9 +72,6 @@ export const MultiRecordSelect = ({
|
|||||||
const relationPickerSearchFilter = useRecoilValue(
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
relationPickerSearchFilterState,
|
relationPickerSearchFilterState,
|
||||||
);
|
);
|
||||||
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
|
|
||||||
leading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHotkeyScope(relationPickerScopedId);
|
setHotkeyScope(relationPickerScopedId);
|
||||||
@@ -86,16 +88,53 @@ export const MultiRecordSelect = ({
|
|||||||
[onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem],
|
[onSubmit, goBackToPreviousHotkeyScope, resetSelectedItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedOnCreate = useDebouncedCallback(
|
|
||||||
() => onCreate?.(relationPickerSearchFilter),
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
(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 (
|
return (
|
||||||
@@ -107,55 +146,30 @@ export const MultiRecordSelect = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu ref={containerRef} data-select-disable>
|
<DropdownMenu ref={containerRef} data-select-disable>
|
||||||
|
{dropdownPlacement?.includes('end') && (
|
||||||
|
<>
|
||||||
|
{createNewButton}
|
||||||
|
{results}
|
||||||
|
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
|
||||||
|
<DropdownMenuSkeletonItem />
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuSearchInput
|
<DropdownMenuSearchInput
|
||||||
value={relationPickerSearchFilter}
|
value={relationPickerSearchFilter}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
{(dropdownPlacement?.includes('start') ||
|
||||||
<DropdownMenuItemsContainer hasMaxHeight hasMinHeight>
|
isUndefinedOrNull(dropdownPlacement)) && (
|
||||||
{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) && (
|
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer>
|
{recordMultiSelectIsLoading && !relationPickerSearchFilter && (
|
||||||
<CreateNewButton
|
<DropdownMenuSkeletonItem />
|
||||||
onClick={debouncedOnCreate}
|
)}
|
||||||
LeftIcon={IconPlus}
|
{results}
|
||||||
text="Add New"
|
{createNewButton}
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type SingleEntitySelectMenuItemsProps = {
|
|||||||
isAllEntitySelectShown?: boolean;
|
isAllEntitySelectShown?: boolean;
|
||||||
onAllEntitySelected?: () => void;
|
onAllEntitySelected?: () => void;
|
||||||
hotkeyScope?: string;
|
hotkeyScope?: string;
|
||||||
|
isFiltered: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleEntitySelectMenuItems = ({
|
export const SingleEntitySelectMenuItems = ({
|
||||||
@@ -54,6 +55,7 @@ export const SingleEntitySelectMenuItems = ({
|
|||||||
isAllEntitySelectShown,
|
isAllEntitySelectShown,
|
||||||
onAllEntitySelected,
|
onAllEntitySelected,
|
||||||
hotkeyScope = RelationPickerHotkeyScope.RelationPicker,
|
hotkeyScope = RelationPickerHotkeyScope.RelationPicker,
|
||||||
|
isFiltered,
|
||||||
}: SingleEntitySelectMenuItemsProps) => {
|
}: SingleEntitySelectMenuItemsProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -139,9 +141,11 @@ export const SingleEntitySelectMenuItems = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{loading ? (
|
{loading && !isFiltered ? (
|
||||||
<DropdownMenuSkeletonItem />
|
<DropdownMenuSkeletonItem />
|
||||||
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
|
) : entitiesInDropdown.length === 0 &&
|
||||||
|
!isAllEntitySelectShown &&
|
||||||
|
!loading ? (
|
||||||
<>
|
<>
|
||||||
<MenuItem text="No result" />
|
<MenuItem text="No result" />
|
||||||
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
{entitiesToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/use
|
|||||||
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
||||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { Placement } from '@floating-ui/react';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
export type SingleEntitySelectMenuItemsWithSearchProps = {
|
||||||
excludedRelationRecordIds?: string[];
|
excludedRelationRecordIds?: string[];
|
||||||
@@ -14,6 +16,7 @@ export type SingleEntitySelectMenuItemsWithSearchProps = {
|
|||||||
relationObjectNameSingular: string;
|
relationObjectNameSingular: string;
|
||||||
relationPickerScopeId?: string;
|
relationPickerScopeId?: string;
|
||||||
selectedRelationRecordIds: string[];
|
selectedRelationRecordIds: string[];
|
||||||
|
dropdownPlacement?: Placement | null;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
SingleEntitySelectMenuItemsProps,
|
SingleEntitySelectMenuItemsProps,
|
||||||
| 'EmptyIcon'
|
| 'EmptyIcon'
|
||||||
@@ -34,6 +37,7 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
|||||||
relationPickerScopeId = 'relation-picker',
|
relationPickerScopeId = 'relation-picker',
|
||||||
selectedEntity,
|
selectedEntity,
|
||||||
selectedRelationRecordIds,
|
selectedRelationRecordIds,
|
||||||
|
dropdownPlacement,
|
||||||
}: SingleEntitySelectMenuItemsWithSearchProps) => {
|
}: SingleEntitySelectMenuItemsWithSearchProps) => {
|
||||||
const { handleSearchFilterChange } = useEntitySelectSearch({
|
const { handleSearchFilterChange } = useEntitySelectSearch({
|
||||||
relationPickerScopeId,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{dropdownPlacement?.includes('end') && (
|
||||||
|
<>
|
||||||
|
{results}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
|
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
|
||||||
<DropdownMenuSeparator />
|
{(dropdownPlacement?.includes('start') ||
|
||||||
<SingleEntitySelectMenuItems
|
isUndefinedOrNull(dropdownPlacement)) && (
|
||||||
entitiesToSelect={entities.entitiesToSelect}
|
<>
|
||||||
loading={entities.loading}
|
<DropdownMenuSeparator />
|
||||||
selectedEntity={
|
{results}
|
||||||
selectedEntity ??
|
</>
|
||||||
(entities.selectedEntities.length === 1
|
)}
|
||||||
? entities.selectedEntities[0]
|
|
||||||
: undefined)
|
|
||||||
}
|
|
||||||
hotkeyScope={relationPickerScopeId}
|
|
||||||
onCreate={onCreateWithInput}
|
|
||||||
{...{
|
|
||||||
EmptyIcon,
|
|
||||||
emptyLabel,
|
|
||||||
onCancel,
|
|
||||||
onEntitySelected,
|
|
||||||
showCreateButton,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Placement,
|
Placement,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
import { MouseEvent, useRef } from 'react';
|
import { MouseEvent, useEffect, useRef } from 'react';
|
||||||
import { Keys } from 'react-hotkeys-hook';
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
@@ -64,8 +64,13 @@ export const Dropdown = ({
|
|||||||
}: DropdownProps) => {
|
}: DropdownProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { isDropdownOpen, toggleDropdown, closeDropdown, dropdownWidth } =
|
const {
|
||||||
useDropdown(dropdownId);
|
isDropdownOpen,
|
||||||
|
toggleDropdown,
|
||||||
|
closeDropdown,
|
||||||
|
dropdownWidth,
|
||||||
|
setDropdownPlacement,
|
||||||
|
} = useDropdown(dropdownId);
|
||||||
|
|
||||||
const offsetMiddlewares = [];
|
const offsetMiddlewares = [];
|
||||||
|
|
||||||
@@ -77,13 +82,17 @@ export const Dropdown = ({
|
|||||||
offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y }));
|
offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { refs, floatingStyles } = useFloating({
|
const { refs, floatingStyles, placement } = useFloating({
|
||||||
placement: dropdownPlacement,
|
placement: dropdownPlacement,
|
||||||
middleware: [flip(), ...offsetMiddlewares],
|
middleware: [flip(), ...offsetMiddlewares],
|
||||||
whileElementsMounted: autoUpdate,
|
whileElementsMounted: autoUpdate,
|
||||||
strategy: dropdownStrategy,
|
strategy: dropdownStrategy,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDropdownPlacement(placement);
|
||||||
|
}, [placement, setDropdownPlacement]);
|
||||||
|
|
||||||
const handleHotkeyTriggered = () => {
|
const handleHotkeyTriggered = () => {
|
||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
|
|||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
|
|
||||||
const StyledDropdownMenuItemsExternalContainer = styled.div<{
|
const StyledDropdownMenuItemsExternalContainer = styled.div<{
|
||||||
hasMinHeight?: boolean;
|
|
||||||
hasMaxHeight?: boolean;
|
hasMaxHeight?: boolean;
|
||||||
}>`
|
}>`
|
||||||
--padding: ${({ theme }) => theme.spacing(1)};
|
--padding: ${({ theme }) => theme.spacing(1)};
|
||||||
@@ -13,7 +12,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{
|
|||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
min-height: ${({ hasMinHeight }) => (hasMinHeight ? '150px' : '100%')};
|
|
||||||
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')};
|
max-height: ${({ hasMaxHeight }) => (hasMaxHeight ? '188px' : 'none')};
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
@@ -38,18 +36,13 @@ const StyledDropdownMenuItemsInternalContainer = styled.div`
|
|||||||
|
|
||||||
export const DropdownMenuItemsContainer = ({
|
export const DropdownMenuItemsContainer = ({
|
||||||
children,
|
children,
|
||||||
hasMinHeight,
|
|
||||||
hasMaxHeight,
|
hasMaxHeight,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
hasMinHeight?: boolean;
|
|
||||||
hasMaxHeight?: boolean;
|
hasMaxHeight?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledDropdownMenuItemsExternalContainer
|
<StyledDropdownMenuItemsExternalContainer hasMaxHeight={hasMaxHeight}>
|
||||||
hasMaxHeight={hasMaxHeight}
|
|
||||||
hasMinHeight={hasMinHeight}
|
|
||||||
>
|
|
||||||
{hasMaxHeight ? (
|
{hasMaxHeight ? (
|
||||||
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
<StyledScrollWrapper contextProviderName="dropdownMenuItemsContainer">
|
||||||
<StyledDropdownMenuItemsInternalContainer>
|
<StyledDropdownMenuItemsInternalContainer>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
|
import { DropdownScopeInternalContext } from '@/ui/layout/dropdown/scopes/scope-internal-context/DropdownScopeInternalContext';
|
||||||
import { dropdownHotkeyComponentState } from '@/ui/layout/dropdown/states/dropdownHotkeyComponentState';
|
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 { dropdownWidthComponentState } from '@/ui/layout/dropdown/states/dropdownWidthComponentState';
|
||||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
@@ -19,6 +20,10 @@ export const useDropdownStates = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
scopeId,
|
scopeId,
|
||||||
|
dropdownPlacementState: extractComponentState(
|
||||||
|
dropdownPlacementComponentState,
|
||||||
|
scopeId,
|
||||||
|
),
|
||||||
dropdownHotkeyScopeState: extractComponentState(
|
dropdownHotkeyScopeState: extractComponentState(
|
||||||
dropdownHotkeyComponentState,
|
dropdownHotkeyComponentState,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
dropdownHotkeyScopeState,
|
dropdownHotkeyScopeState,
|
||||||
dropdownWidthState,
|
dropdownWidthState,
|
||||||
isDropdownOpenState,
|
isDropdownOpenState,
|
||||||
|
dropdownPlacementState,
|
||||||
} = useDropdownStates({
|
} = useDropdownStates({
|
||||||
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
|
dropdownScopeId: getScopeIdOrUndefinedFromComponentId(dropdownId),
|
||||||
});
|
});
|
||||||
@@ -25,6 +26,10 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
|
|
||||||
const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState);
|
const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState);
|
||||||
|
|
||||||
|
const [dropdownPlacement, setDropdownPlacement] = useRecoilState(
|
||||||
|
dropdownPlacementState,
|
||||||
|
);
|
||||||
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] =
|
const [isDropdownOpen, setIsDropdownOpen] =
|
||||||
useRecoilState(isDropdownOpenState);
|
useRecoilState(isDropdownOpenState);
|
||||||
|
|
||||||
@@ -59,5 +64,7 @@ export const useDropdown = (dropdownId?: string) => {
|
|||||||
openDropdown,
|
openDropdown,
|
||||||
dropdownWidth,
|
dropdownWidth,
|
||||||
setDropdownWidth,
|
setDropdownWidth,
|
||||||
|
dropdownPlacement,
|
||||||
|
setDropdownPlacement,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user