diff --git a/front/src/modules/ui/input/relation-picker/components/CreateNewButton.tsx b/front/src/modules/ui/input/relation-picker/components/CreateNewButton.tsx
new file mode 100644
index 000000000..06d4f836f
--- /dev/null
+++ b/front/src/modules/ui/input/relation-picker/components/CreateNewButton.tsx
@@ -0,0 +1,14 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+import { MenuItem } from '@/ui/menu-item/components/MenuItem';
+
+const StyledCreateNewButton = styled(MenuItem)<{ hovered: boolean }>`
+ ${({ hovered, theme }) =>
+ hovered &&
+ css`
+ background: ${theme.background.transparent.light};
+ `}
+`;
+
+export const CreateNewButton = StyledCreateNewButton;
diff --git a/front/src/modules/ui/input/relation-picker/components/SingleEntitySelect.tsx b/front/src/modules/ui/input/relation-picker/components/SingleEntitySelect.tsx
index 26bb28075..357b07254 100644
--- a/front/src/modules/ui/input/relation-picker/components/SingleEntitySelect.tsx
+++ b/front/src/modules/ui/input/relation-picker/components/SingleEntitySelect.tsx
@@ -2,10 +2,7 @@ import { useRef } from 'react';
import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
-import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
-import { IconPlus } from '@/ui/icon';
-import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
@@ -59,6 +56,7 @@ export const SingleEntitySelect = <
onCancel?.();
},
});
+
return (
-
- {showCreateButton && (
- <>
-
-
-
-
- >
- )}
+
);
};
diff --git a/front/src/modules/ui/input/relation-picker/components/SingleEntitySelectBase.tsx b/front/src/modules/ui/input/relation-picker/components/SingleEntitySelectBase.tsx
index a1ebf4165..0da136af8 100644
--- a/front/src/modules/ui/input/relation-picker/components/SingleEntitySelectBase.tsx
+++ b/front/src/modules/ui/input/relation-picker/components/SingleEntitySelectBase.tsx
@@ -2,19 +2,24 @@ import { useRef } from 'react';
import { Key } from 'ts-key-enum';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
+import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
+import { IconPlus } from '@/ui/icon';
import type { IconComponent } from '@/ui/icon/types/IconComponent';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
+import { MenuItemSelect } from '@/ui/menu-item/components/MenuItemSelect';
import { MenuItemSelectAvatar } from '@/ui/menu-item/components/MenuItemSelectAvatar';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Avatar } from '@/users/components/Avatar';
import { assertNotNull } from '~/utils/assert';
import { isNonEmptyString } from '~/utils/isNonEmptyString';
+import { CreateButtonId, EmptyButtonId } from '../constants';
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
import { EntityForSelect } from '../types/EntityForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
import { DropdownMenuSkeletonItem } from './skeletons/DropdownMenuSkeletonItem';
+import { CreateNewButton } from './CreateNewButton';
export type SingleEntitySelectBaseProps<
CustomEntityForSelect extends EntityForSelect,
@@ -26,6 +31,8 @@ export type SingleEntitySelectBaseProps<
onCancel?: () => void;
onEntitySelected: (entity?: CustomEntityForSelect) => void;
selectedEntity?: CustomEntityForSelect;
+ onCreate?: () => void;
+ showCreateButton?: boolean;
};
export const SingleEntitySelectBase = <
@@ -38,6 +45,8 @@ export const SingleEntitySelectBase = <
onCancel,
onEntitySelected,
selectedEntity,
+ onCreate,
+ showCreateButton,
}: SingleEntitySelectBaseProps) => {
const containerRef = useRef(null);
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
@@ -45,19 +54,31 @@ export const SingleEntitySelectBase = <
assertNotNull(entity) && isNonEmptyString(entity.name.trim()),
);
- const { hoveredIndex, resetScroll } = useEntitySelectScroll({
- entities: entitiesInDropdown,
+ const { preselectedOptionId, resetScroll } = useEntitySelectScroll({
+ selectableOptionIds: [
+ EmptyButtonId,
+ ...entitiesInDropdown.map((item) => item.id),
+ ...(showCreateButton ? [CreateButtonId] : []),
+ ],
containerRef,
});
useScopedHotkeys(
Key.Enter,
() => {
- onEntitySelected(entitiesInDropdown[hoveredIndex]);
+ if (showCreateButton && preselectedOptionId === CreateButtonId) {
+ onCreate?.();
+ } else {
+ const entity = entitiesInDropdown.findIndex(
+ (entity) => entity.id === preselectedOptionId,
+ );
+ onEntitySelected(entitiesInDropdown[entity]);
+ }
+
resetScroll();
},
RelationPickerHotkeyScope.RelationPicker,
- [entitiesInDropdown, hoveredIndex, onEntitySelected],
+ [entitiesInDropdown, preselectedOptionId, onEntitySelected],
);
useScopedHotkeys(
@@ -70,39 +91,56 @@ export const SingleEntitySelectBase = <
);
return (
-
- {emptyLabel && (
-
+ >
);
};
diff --git a/front/src/modules/ui/input/relation-picker/constants/index.ts b/front/src/modules/ui/input/relation-picker/constants/index.ts
new file mode 100644
index 000000000..4521251b9
--- /dev/null
+++ b/front/src/modules/ui/input/relation-picker/constants/index.ts
@@ -0,0 +1,2 @@
+export const CreateButtonId = 'create-button';
+export const EmptyButtonId = 'empty-button';
diff --git a/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectScroll.ts b/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectScroll.ts
index 67d78b391..88c8f0f3e 100644
--- a/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectScroll.ts
+++ b/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectScroll.ts
@@ -4,28 +4,36 @@ import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
-import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
-import { EntityForSelect } from '../types/EntityForSelect';
+import { CreateButtonId } from '../constants';
+import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
+import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
+import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
-export const useEntitySelectScroll = <
- CustomEntityForSelect extends EntityForSelect,
->({
+export const useEntitySelectScroll = ({
containerRef,
- entities,
+ selectableOptionIds,
}: {
- entities: CustomEntityForSelect[];
+ selectableOptionIds: string[];
containerRef: React.RefObject;
}) => {
- const [relationPickerHoverIndex, setRelationPickerHoverIndex] =
- useRecoilScopedState(relationPickerHoverIndexScopedState);
+ const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
+ useRecoilScopedState(
+ relationPickerPreselectedIdScopedState,
+ RelationPickerRecoilScopeContext,
+ );
+
+ const preselectedIdIndex = getPreselectedIdIndex(
+ selectableOptionIds,
+ relationPickerPreselectedId,
+ );
const resetScroll = () => {
- setRelationPickerHoverIndex(0);
+ setRelationPickerPreselectedId('');
- const currentHoveredRef = containerRef.current?.children[0] as HTMLElement;
+ const preselectedRef = containerRef.current?.children[0] as HTMLElement;
- scrollIntoView(currentHoveredRef, {
+ scrollIntoView(preselectedRef, {
align: {
top: 0,
},
@@ -39,16 +47,15 @@ export const useEntitySelectScroll = <
useScopedHotkeys(
Key.ArrowUp,
() => {
- setRelationPickerHoverIndex((prevSelectedIndex) =>
- Math.max(prevSelectedIndex - 1, 0),
- );
-
- const currentHoveredRef = containerRef.current?.children[
- relationPickerHoverIndex
+ const previousSelectableIndex = Math.max(preselectedIdIndex - 1, 0);
+ const previousSelectableId = selectableOptionIds[previousSelectableIndex];
+ setRelationPickerPreselectedId(previousSelectableId);
+ const preselectedRef = containerRef.current?.children[
+ previousSelectableIndex
] as HTMLElement;
- if (currentHoveredRef) {
- scrollIntoView(currentHoveredRef, {
+ if (preselectedRef) {
+ scrollIntoView(preselectedRef, {
align: {
top: 0.5,
},
@@ -60,38 +67,40 @@ export const useEntitySelectScroll = <
}
},
RelationPickerHotkeyScope.RelationPicker,
- [setRelationPickerHoverIndex, entities],
+ [selectableOptionIds],
);
useScopedHotkeys(
Key.ArrowDown,
() => {
- setRelationPickerHoverIndex((prevSelectedIndex) =>
- Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
+ const nextSelectableIndex = Math.min(
+ preselectedIdIndex + 1,
+ selectableOptionIds?.length - 1,
);
+ const nextSelectableId = selectableOptionIds[nextSelectableIndex];
+ setRelationPickerPreselectedId(nextSelectableId);
+ if (nextSelectableId !== CreateButtonId) {
+ const preselectedRef = containerRef.current?.children[
+ nextSelectableIndex
+ ] as HTMLElement;
- const currentHoveredRef = containerRef.current?.children[
- relationPickerHoverIndex
- ] as HTMLElement;
-
- if (currentHoveredRef) {
- scrollIntoView(currentHoveredRef, {
- align: {
- top: 0.15,
- },
- isScrollable: (target) => {
- return target === containerRef.current;
- },
- time: 0,
- });
+ if (preselectedRef) {
+ scrollIntoView(preselectedRef, {
+ align: {
+ top: 0.15,
+ },
+ isScrollable: (target) => target === containerRef.current,
+ time: 0,
+ });
+ }
}
},
RelationPickerHotkeyScope.RelationPicker,
- [setRelationPickerHoverIndex, entities],
+ [selectableOptionIds],
);
return {
- hoveredIndex: relationPickerHoverIndex,
+ preselectedOptionId: relationPickerPreselectedId,
resetScroll,
};
};
diff --git a/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectSearch.ts b/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectSearch.ts
index 3858fe757..e9a15af31 100644
--- a/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectSearch.ts
+++ b/front/src/modules/ui/input/relation-picker/hooks/useEntitySelectSearch.ts
@@ -3,12 +3,14 @@ import debounce from 'lodash.debounce';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
-import { relationPickerHoverIndexScopedState } from '../states/relationPickerHoverIndexScopedState';
+import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
+import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
export const useEntitySelectSearch = () => {
- const [, setRelationPickerHoverIndex] = useRecoilScopedState(
- relationPickerHoverIndexScopedState,
+ const [, setRelationPickerPreselectedId] = useRecoilScopedState(
+ relationPickerPreselectedIdScopedState,
+ RelationPickerRecoilScopeContext,
);
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
@@ -26,7 +28,7 @@ export const useEntitySelectSearch = () => {
event: React.ChangeEvent,
) => {
debouncedSetSearchFilter(event.currentTarget.value);
- setRelationPickerHoverIndex(0);
+ setRelationPickerPreselectedId('');
};
useEffect(() => {
diff --git a/front/src/modules/ui/input/relation-picker/states/recoil-scope-contexts/RelationPickerRecoilScopeContext.ts b/front/src/modules/ui/input/relation-picker/states/recoil-scope-contexts/RelationPickerRecoilScopeContext.ts
new file mode 100644
index 000000000..25dbee7f3
--- /dev/null
+++ b/front/src/modules/ui/input/relation-picker/states/recoil-scope-contexts/RelationPickerRecoilScopeContext.ts
@@ -0,0 +1,5 @@
+import { createContext } from 'react';
+
+export const RelationPickerRecoilScopeContext = createContext(
+ 'relation-picker-context',
+);
diff --git a/front/src/modules/ui/input/relation-picker/states/relationPickerHoverIndexScopedState.ts b/front/src/modules/ui/input/relation-picker/states/relationPickerHoverIndexScopedState.ts
deleted file mode 100644
index dad7085d7..000000000
--- a/front/src/modules/ui/input/relation-picker/states/relationPickerHoverIndexScopedState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { atomFamily } from 'recoil';
-
-export const relationPickerHoverIndexScopedState = atomFamily({
- key: 'relationPickerHoverIndexScopedState',
- default: 0,
-});
diff --git a/front/src/modules/ui/input/relation-picker/states/relationPickerPreselectedIdScopedState.ts b/front/src/modules/ui/input/relation-picker/states/relationPickerPreselectedIdScopedState.ts
new file mode 100644
index 000000000..101f15a50
--- /dev/null
+++ b/front/src/modules/ui/input/relation-picker/states/relationPickerPreselectedIdScopedState.ts
@@ -0,0 +1,9 @@
+import { atomFamily } from 'recoil';
+
+export const relationPickerPreselectedIdScopedState = atomFamily<
+ string,
+ string
+>({
+ key: 'relationPickerPreselectedIdScopedState',
+ default: (param) => param,
+});
diff --git a/front/src/modules/ui/input/relation-picker/utils/getPreselectedIdIndex.ts b/front/src/modules/ui/input/relation-picker/utils/getPreselectedIdIndex.ts
new file mode 100644
index 000000000..f42631ec2
--- /dev/null
+++ b/front/src/modules/ui/input/relation-picker/utils/getPreselectedIdIndex.ts
@@ -0,0 +1,10 @@
+export const getPreselectedIdIndex = (
+ selectableOptionIds: string[],
+ preselectedOptionId: string,
+) => {
+ const preselectedIdIndex = selectableOptionIds.findIndex(
+ (option) => option === preselectedOptionId,
+ );
+
+ return preselectedIdIndex === -1 ? 0 : preselectedIdIndex;
+};