mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): rework add node select
- `space` and `/` open floating add node select - improved filter logic (partial word matches)
This commit is contained in:
parent
a9fdc77edd
commit
4be4fc6731
@ -9,6 +9,7 @@ import 'reactflow/dist/style.css';
|
||||
import NodeEditorPanelGroup from './sidePanel/NodeEditorPanelGroup';
|
||||
import { Flow } from './flow/Flow';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
|
||||
|
||||
const NodeEditor = () => {
|
||||
const [isPanelCollapsed, setIsPanelCollapsed] = useState(false);
|
||||
@ -57,9 +58,10 @@ const NodeEditor = () => {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={{ position: 'relative', width: '100%', height: '100%' }}
|
||||
>
|
||||
<Flow />
|
||||
<AddNodePopover />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
@ -0,0 +1,205 @@
|
||||
import {
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData';
|
||||
import {
|
||||
addNodePopoverClosed,
|
||||
addNodePopoverOpened,
|
||||
nodeAdded,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
import { map } from 'lodash-es';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import { AddNodePopoverSelectItem } from './AddNodePopoverSelectItem';
|
||||
|
||||
type NodeTemplate = {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
const filter = (value: string, item: NodeTemplate) => {
|
||||
const regex = new RegExp(
|
||||
value
|
||||
.trim()
|
||||
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
||||
.split(' ')
|
||||
.join('.*'),
|
||||
'gi'
|
||||
);
|
||||
return (
|
||||
regex.test(item.label) ||
|
||||
regex.test(item.description) ||
|
||||
item.tags.some((tag) => regex.test(tag))
|
||||
);
|
||||
};
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
({ nodes }) => {
|
||||
const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => {
|
||||
return {
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
tags: template.tags,
|
||||
};
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Progress Image',
|
||||
value: 'current_image',
|
||||
description: 'Displays the current image in the Node Editor',
|
||||
tags: ['progress'],
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Notes',
|
||||
value: 'notes',
|
||||
description: 'Add notes about your workflow',
|
||||
tags: ['notes'],
|
||||
});
|
||||
|
||||
data.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return { data };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const AddNodePopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const buildInvocation = useBuildNodeData();
|
||||
const toaster = useAppToaster();
|
||||
const { data } = useAppSelector(selector);
|
||||
const isOpen = useAppSelector((state) => state.nodes.isAddNodePopoverOpen);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: AnyInvocationType) => {
|
||||
const invocation = buildInvocation(nodeType);
|
||||
|
||||
if (!invocation) {
|
||||
toaster({
|
||||
status: 'error',
|
||||
title: `Unknown Invocation type ${nodeType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(nodeAdded(invocation));
|
||||
},
|
||||
[dispatch, buildInvocation, toaster]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: string | null) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNode(v as AnyInvocationType);
|
||||
},
|
||||
[addNode]
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
dispatch(addNodePopoverClosed());
|
||||
}, [dispatch]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
dispatch(addNodePopoverOpened());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleHotkeyOpen: HotkeyCallback = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
onOpen();
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
},
|
||||
[onOpen]
|
||||
);
|
||||
|
||||
const handleHotkeyClose: HotkeyCallback = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
useHotkeys(['space', '/'], handleHotkeyOpen);
|
||||
useHotkeys(['escape'], handleHotkeyClose);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
initialFocusRef={inputRef}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
placement="bottom"
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
closeOnBlur={true}
|
||||
returnFocusOnClose={true}
|
||||
>
|
||||
<PopoverAnchor>
|
||||
<Flex
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
insetInlineStart: '50%',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
sx={{
|
||||
p: 0,
|
||||
top: -1,
|
||||
shadow: 'dark-lg',
|
||||
borderColor: 'accent.300',
|
||||
borderWidth: '2px',
|
||||
borderStyle: 'solid',
|
||||
_dark: { borderColor: 'accent.400' },
|
||||
}}
|
||||
>
|
||||
<PopoverBody sx={{ p: 0 }}>
|
||||
<IAIMantineSearchableSelect
|
||||
inputRef={inputRef}
|
||||
selectOnBlur={false}
|
||||
placeholder="Search for nodes"
|
||||
value={null}
|
||||
data={data}
|
||||
maxDropdownHeight={400}
|
||||
nothingFound="No matching nodes"
|
||||
itemComponent={AddNodePopoverSelectItem}
|
||||
filter={filter}
|
||||
onChange={handleChange}
|
||||
hoverOnSearchChange={true}
|
||||
onDropdownClose={onClose}
|
||||
sx={{
|
||||
width: '32rem',
|
||||
input: {
|
||||
padding: '0.5rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AddNodePopover);
|
@ -0,0 +1,29 @@
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { forwardRef } from 'react';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const AddNodePopoverSelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ label, description, ...others }: ItemProps, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...others}>
|
||||
<div>
|
||||
<Text fontWeight={600}>{label}</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
sx={{ color: 'base.600', _dark: { color: 'base.500' } }}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AddNodePopoverSelectItem.displayName = 'AddNodePopoverSelectItem';
|
@ -5,14 +5,14 @@ import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FaExpand,
|
||||
FaInfo,
|
||||
// FaInfo,
|
||||
FaMapMarkerAlt,
|
||||
FaMinus,
|
||||
FaPlus,
|
||||
} from 'react-icons/fa';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import {
|
||||
shouldShowFieldTypeLegendChanged,
|
||||
// shouldShowFieldTypeLegendChanged,
|
||||
shouldShowMinimapPanelChanged,
|
||||
} from 'features/nodes/store/nodesSlice';
|
||||
|
||||
@ -20,9 +20,9 @@ const ViewportControls = () => {
|
||||
const { t } = useTranslation();
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldShowFieldTypeLegend = useAppSelector(
|
||||
(state) => state.nodes.shouldShowFieldTypeLegend
|
||||
);
|
||||
// const shouldShowFieldTypeLegend = useAppSelector(
|
||||
// (state) => state.nodes.shouldShowFieldTypeLegend
|
||||
// );
|
||||
const shouldShowMinimapPanel = useAppSelector(
|
||||
(state) => state.nodes.shouldShowMinimapPanel
|
||||
);
|
||||
@ -39,9 +39,9 @@ const ViewportControls = () => {
|
||||
fitView();
|
||||
}, [fitView]);
|
||||
|
||||
const handleClickedToggleFieldTypeLegend = useCallback(() => {
|
||||
dispatch(shouldShowFieldTypeLegendChanged(!shouldShowFieldTypeLegend));
|
||||
}, [shouldShowFieldTypeLegend, dispatch]);
|
||||
// const handleClickedToggleFieldTypeLegend = useCallback(() => {
|
||||
// dispatch(shouldShowFieldTypeLegendChanged(!shouldShowFieldTypeLegend));
|
||||
// }, [shouldShowFieldTypeLegend, dispatch]);
|
||||
|
||||
const handleClickedToggleMiniMapPanel = useCallback(() => {
|
||||
dispatch(shouldShowMinimapPanelChanged(!shouldShowMinimapPanel));
|
||||
@ -70,7 +70,7 @@ const ViewportControls = () => {
|
||||
icon={<FaExpand />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
{/* <Tooltip
|
||||
label={
|
||||
shouldShowFieldTypeLegend
|
||||
? t('nodes.hideLegendNodes')
|
||||
@ -83,7 +83,7 @@ const ViewportControls = () => {
|
||||
onClick={handleClickedToggleFieldTypeLegend}
|
||||
icon={<FaInfo />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Tooltip> */}
|
||||
<Tooltip
|
||||
label={
|
||||
shouldShowMinimapPanel
|
||||
|
@ -1,155 +0,0 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import IAIMantineSearchableSelect from 'common/components/IAIMantineSearchableSelect';
|
||||
import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData';
|
||||
import { nodeAdded } from 'features/nodes/store/nodesSlice';
|
||||
import { map } from 'lodash-es';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
|
||||
type NodeTemplate = {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
};
|
||||
const filter = (value: string, item: NodeTemplate) => {
|
||||
const regex = new RegExp(
|
||||
value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
// strip out regex special characters to avoid errors
|
||||
.replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
|
||||
.split(' ')
|
||||
.join('.*'),
|
||||
'g'
|
||||
);
|
||||
return (
|
||||
regex.test(item.label.toLowerCase()) ||
|
||||
regex.test(item.description.toLowerCase()) ||
|
||||
item.tags.some((tag) => regex.test(tag))
|
||||
);
|
||||
};
|
||||
|
||||
const selector = createSelector(
|
||||
[stateSelector],
|
||||
({ nodes }) => {
|
||||
const data: NodeTemplate[] = map(nodes.nodeTemplates, (template) => {
|
||||
return {
|
||||
label: template.title,
|
||||
value: template.type,
|
||||
description: template.description,
|
||||
tags: template.tags,
|
||||
};
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Progress Image',
|
||||
value: 'current_image',
|
||||
description: 'Displays the current image in the Node Editor',
|
||||
tags: ['progress'],
|
||||
});
|
||||
|
||||
data.push({
|
||||
label: 'Notes',
|
||||
value: 'notes',
|
||||
description: 'Add notes about your workflow',
|
||||
tags: ['notes'],
|
||||
});
|
||||
|
||||
data.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return { data };
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
const AddNodeMenu = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { data } = useAppSelector(selector);
|
||||
|
||||
const buildInvocation = useBuildNodeData();
|
||||
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: AnyInvocationType) => {
|
||||
const invocation = buildInvocation(nodeType);
|
||||
|
||||
if (!invocation) {
|
||||
toaster({
|
||||
status: 'error',
|
||||
title: `Unknown Invocation type ${nodeType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(nodeAdded(invocation));
|
||||
},
|
||||
[dispatch, buildInvocation, toaster]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: string | null) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNode(v as AnyInvocationType);
|
||||
},
|
||||
[addNode]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex sx={{ gap: 2, alignItems: 'center' }}>
|
||||
<IAIMantineSearchableSelect
|
||||
selectOnBlur={false}
|
||||
placeholder="Add Node"
|
||||
value={null}
|
||||
data={data}
|
||||
maxDropdownHeight={400}
|
||||
nothingFound="No matching nodes"
|
||||
itemComponent={SelectItem}
|
||||
filter={filter}
|
||||
onChange={handleChange}
|
||||
hoverOnSearchChange={true}
|
||||
sx={{
|
||||
width: '24rem',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ label, description, ...others }: ItemProps, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...others}>
|
||||
<div>
|
||||
<Text fontWeight={600}>{label}</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
sx={{ color: 'base.600', _dark: { color: 'base.500' } }}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectItem.displayName = 'SelectItem';
|
||||
|
||||
export default AddNodeMenu;
|
@ -1,11 +1,27 @@
|
||||
import { memo } from 'react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { Panel } from 'reactflow';
|
||||
import AddNodeMenu from './AddNodeMenu';
|
||||
|
||||
const TopLeftPanel = () => (
|
||||
<Panel position="top-left">
|
||||
<AddNodeMenu />
|
||||
</Panel>
|
||||
);
|
||||
const TopLeftPanel = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleOpenAddNodePopover = useCallback(() => {
|
||||
dispatch(addNodePopoverOpened());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Panel position="top-left">
|
||||
<IAIIconButton
|
||||
aria-label="Add Node"
|
||||
tooltip="Add Node"
|
||||
onClick={handleOpenAddNodePopover}
|
||||
icon={<FaPlus />}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TopLeftPanel);
|
||||
|
@ -1,208 +0,0 @@
|
||||
import { Box, Flex } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIInput from 'common/components/IAIInput';
|
||||
import { useBuildNodeData } from 'features/nodes/hooks/useBuildNodeData';
|
||||
import { InvocationTemplate } from 'features/nodes/types/types';
|
||||
import Fuse from 'fuse.js';
|
||||
import { map } from 'lodash-es';
|
||||
import {
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
ReactNode,
|
||||
memo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import { nodeAdded } from '../../store/nodesSlice';
|
||||
|
||||
interface NodeListItemProps {
|
||||
title: string;
|
||||
description: string;
|
||||
type: AnyInvocationType;
|
||||
isSelected: boolean;
|
||||
addNode: (nodeType: AnyInvocationType) => void;
|
||||
}
|
||||
|
||||
const NodeListItem = (props: NodeListItemProps) => {
|
||||
const { title, description, type, isSelected, addNode } = props;
|
||||
return (
|
||||
<Tooltip label={description} placement="end" hasArrow>
|
||||
<Box
|
||||
px={4}
|
||||
onClick={() => addNode(type)}
|
||||
background={isSelected ? 'base.600' : 'none'}
|
||||
_hover={{
|
||||
background: 'base.600',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
NodeListItem.displayName = 'NodeListItem';
|
||||
|
||||
const NodeSearch = () => {
|
||||
const nodeTemplates = useAppSelector((state) =>
|
||||
map(state.nodes.nodeTemplates)
|
||||
);
|
||||
|
||||
const [filteredNodes, setFilteredNodes] = useState<
|
||||
Fuse.FuseResult<InvocationTemplate>[]
|
||||
>([]);
|
||||
|
||||
const buildInvocation = useBuildNodeData();
|
||||
const dispatch = useAppDispatch();
|
||||
const toaster = useAppToaster();
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [showNodeList, setShowNodeList] = useState<boolean>(false);
|
||||
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
|
||||
const nodeSearchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fuseOptions = {
|
||||
findAllMatches: true,
|
||||
threshold: 0,
|
||||
ignoreLocation: true,
|
||||
keys: ['title', 'type', 'tags'],
|
||||
};
|
||||
|
||||
const fuse = new Fuse(nodeTemplates, fuseOptions);
|
||||
|
||||
const findNode = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
setFilteredNodes(fuse.search(e.target.value));
|
||||
setShowNodeList(true);
|
||||
};
|
||||
|
||||
const addNode = useCallback(
|
||||
(nodeType: AnyInvocationType) => {
|
||||
const invocation = buildInvocation(nodeType);
|
||||
|
||||
if (!invocation) {
|
||||
toaster({
|
||||
status: 'error',
|
||||
title: `Unknown Invocation type ${nodeType}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(nodeAdded(invocation));
|
||||
},
|
||||
[dispatch, buildInvocation, toaster]
|
||||
);
|
||||
|
||||
const renderNodeList = () => {
|
||||
const nodeListToRender: ReactNode[] = [];
|
||||
|
||||
if (searchText.length > 0) {
|
||||
filteredNodes.forEach(({ item }, index) => {
|
||||
const { title, description, type } = item;
|
||||
if (title.toLowerCase().includes(searchText)) {
|
||||
nodeListToRender.push(
|
||||
<NodeListItem
|
||||
key={index}
|
||||
title={title}
|
||||
description={description}
|
||||
type={type}
|
||||
isSelected={focusedIndex === index}
|
||||
addNode={addNode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
nodeTemplates.forEach(({ title, description, type }, index) => {
|
||||
nodeListToRender.push(
|
||||
<NodeListItem
|
||||
key={index}
|
||||
title={title}
|
||||
description={description}
|
||||
type={type}
|
||||
isSelected={focusedIndex === index}
|
||||
addNode={addNode}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" background="base.900" borderRadius={6}>
|
||||
{nodeListToRender}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const searchKeyHandler = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
const { key } = e;
|
||||
let nextIndex = 0;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
setShowNodeList(true);
|
||||
if (searchText.length > 0) {
|
||||
nextIndex = (focusedIndex + 1) % filteredNodes.length;
|
||||
} else {
|
||||
nextIndex = (focusedIndex + 1) % nodeTemplates.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'ArrowUp') {
|
||||
setShowNodeList(true);
|
||||
if (searchText.length > 0) {
|
||||
nextIndex =
|
||||
(focusedIndex + filteredNodes.length - 1) % filteredNodes.length;
|
||||
} else {
|
||||
nextIndex =
|
||||
(focusedIndex + nodeTemplates.length - 1) % nodeTemplates.length;
|
||||
}
|
||||
}
|
||||
|
||||
// # TODO Handle Blur
|
||||
// if (key === 'Escape') {
|
||||
// }
|
||||
|
||||
if (key === 'Enter') {
|
||||
let selectedNodeType: AnyInvocationType | undefined;
|
||||
|
||||
if (searchText.length > 0) {
|
||||
selectedNodeType = filteredNodes[focusedIndex]?.item.type;
|
||||
} else {
|
||||
selectedNodeType = nodeTemplates[focusedIndex]?.type;
|
||||
}
|
||||
|
||||
if (selectedNodeType) {
|
||||
addNode(selectedNodeType);
|
||||
}
|
||||
setShowNodeList(false);
|
||||
}
|
||||
|
||||
setFocusedIndex(nextIndex);
|
||||
};
|
||||
|
||||
const searchInputBlurHandler = (e: FocusEvent<HTMLDivElement>) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) setShowNodeList(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
tabIndex={1}
|
||||
onKeyDown={searchKeyHandler}
|
||||
onFocus={() => setShowNodeList(true)}
|
||||
onBlur={searchInputBlurHandler}
|
||||
ref={nodeSearchRef}
|
||||
>
|
||||
<IAIInput value={searchText} onChange={findNode} />
|
||||
{showNodeList && renderNodeList()}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeSearch);
|
@ -78,6 +78,7 @@ export const initialNodesState: NodesState = {
|
||||
shouldAnimateEdges: true,
|
||||
shouldSnapToGrid: false,
|
||||
shouldColorEdges: true,
|
||||
isAddNodePopoverOpen: false,
|
||||
nodeOpacity: 1,
|
||||
selectedNodes: [],
|
||||
selectedEdges: [],
|
||||
@ -699,6 +700,15 @@ const nodesSlice = createSlice({
|
||||
};
|
||||
});
|
||||
},
|
||||
addNodePopoverOpened: (state) => {
|
||||
state.isAddNodePopoverOpen = true;
|
||||
},
|
||||
addNodePopoverClosed: (state) => {
|
||||
state.isAddNodePopoverOpen = false;
|
||||
},
|
||||
addNodePopoverToggled: (state) => {
|
||||
state.isAddNodePopoverOpen = !state.isAddNodePopoverOpen;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||
@ -812,6 +822,9 @@ export const {
|
||||
selectionCopied,
|
||||
selectionPasted,
|
||||
selectedAll,
|
||||
addNodePopoverOpened,
|
||||
addNodePopoverClosed,
|
||||
addNodePopoverToggled,
|
||||
} = nodesSlice.actions;
|
||||
|
||||
export default nodesSlice.reducer;
|
||||
|
@ -33,4 +33,5 @@ export type NodesState = {
|
||||
mouseOverField: FieldIdentifier | null;
|
||||
nodesToCopy: Node<NodeData>[];
|
||||
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
||||
isAddNodePopoverOpen: boolean;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user