(fix) do not transitively rely on deprecated lodash deps (#175)
Signed-off-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
committed by
GitHub
parent
057f113c6d
commit
f3f9e41a15
21
codex-cli/src/components/select-input/Indicator.tsx
Normal file
21
codex-cli/src/components/select-input/Indicator.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import figures from "figures";
|
||||
import { Box, Text } from "ink";
|
||||
import React from "react";
|
||||
|
||||
export type Props = {
|
||||
readonly isSelected?: boolean;
|
||||
};
|
||||
|
||||
function Indicator({ isSelected = false }: Props): JSX.Element {
|
||||
return (
|
||||
<Box marginRight={1}>
|
||||
{isSelected ? (
|
||||
<Text color="blue">{figures.pointer}</Text>
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Indicator;
|
||||
13
codex-cli/src/components/select-input/Item.tsx
Normal file
13
codex-cli/src/components/select-input/Item.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Text } from "ink";
|
||||
import * as React from "react";
|
||||
|
||||
export type Props = {
|
||||
readonly isSelected?: boolean;
|
||||
readonly label: string;
|
||||
};
|
||||
|
||||
function Item({ isSelected = false, label }: Props): JSX.Element {
|
||||
return <Text color={isSelected ? "blue" : undefined}>{label}</Text>;
|
||||
}
|
||||
|
||||
export default Item;
|
||||
189
codex-cli/src/components/select-input/select-input.tsx
Normal file
189
codex-cli/src/components/select-input/select-input.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import Indicator, { type Props as IndicatorProps } from "./Indicator.js";
|
||||
import ItemComponent, { type Props as ItemProps } from "./Item.js";
|
||||
import isEqual from "fast-deep-equal";
|
||||
import { Box, useInput } from "ink";
|
||||
import React, {
|
||||
type FC,
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import arrayToRotated from "to-rotated";
|
||||
|
||||
type Props<V> = {
|
||||
/**
|
||||
* Items to display in a list. Each item must be an object and have `label` and `value` props, it may also optionally have a `key` prop.
|
||||
* If no `key` prop is provided, `value` will be used as the item key.
|
||||
*/
|
||||
readonly items?: Array<Item<V>>;
|
||||
|
||||
/**
|
||||
* Listen to user's input. Useful in case there are multiple input components at the same time and input must be "routed" to a specific component.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
readonly isFocused?: boolean;
|
||||
|
||||
/**
|
||||
* Index of initially-selected item in `items` array.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
readonly initialIndex?: number;
|
||||
|
||||
/**
|
||||
* Number of items to display.
|
||||
*/
|
||||
readonly limit?: number;
|
||||
|
||||
/**
|
||||
* Custom component to override the default indicator component.
|
||||
*/
|
||||
readonly indicatorComponent?: FC<IndicatorProps>;
|
||||
|
||||
/**
|
||||
* Custom component to override the default item component.
|
||||
*/
|
||||
readonly itemComponent?: FC<ItemProps>;
|
||||
|
||||
/**
|
||||
* Function to call when user selects an item. Item object is passed to that function as an argument.
|
||||
*/
|
||||
readonly onSelect?: (item: Item<V>) => void;
|
||||
|
||||
/**
|
||||
* Function to call when user highlights an item. Item object is passed to that function as an argument.
|
||||
*/
|
||||
readonly onHighlight?: (item: Item<V>) => void;
|
||||
};
|
||||
|
||||
export type Item<V> = {
|
||||
key?: string;
|
||||
label: string;
|
||||
value: V;
|
||||
};
|
||||
|
||||
function SelectInput<V>({
|
||||
items = [],
|
||||
isFocused = true,
|
||||
initialIndex = 0,
|
||||
indicatorComponent = Indicator,
|
||||
itemComponent = ItemComponent,
|
||||
limit: customLimit,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
}: Props<V>): JSX.Element {
|
||||
const hasLimit =
|
||||
typeof customLimit === "number" && items.length > customLimit;
|
||||
const limit = hasLimit ? Math.min(customLimit, items.length) : items.length;
|
||||
const lastIndex = limit - 1;
|
||||
const [rotateIndex, setRotateIndex] = useState(
|
||||
initialIndex > lastIndex ? lastIndex - initialIndex : 0,
|
||||
);
|
||||
const [selectedIndex, setSelectedIndex] = useState(
|
||||
initialIndex ? (initialIndex > lastIndex ? lastIndex : initialIndex) : 0,
|
||||
);
|
||||
const previousItems = useRef<Array<Item<V>>>(items);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isEqual(
|
||||
previousItems.current.map((item) => item.value),
|
||||
items.map((item) => item.value),
|
||||
)
|
||||
) {
|
||||
setRotateIndex(0);
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
|
||||
previousItems.current = items;
|
||||
}, [items]);
|
||||
|
||||
useInput(
|
||||
useCallback(
|
||||
(input, key) => {
|
||||
if (input === "k" || key.upArrow) {
|
||||
const lastIndex = (hasLimit ? limit : items.length) - 1;
|
||||
const atFirstIndex = selectedIndex === 0;
|
||||
const nextIndex = hasLimit ? selectedIndex : lastIndex;
|
||||
const nextRotateIndex = atFirstIndex ? rotateIndex + 1 : rotateIndex;
|
||||
const nextSelectedIndex = atFirstIndex
|
||||
? nextIndex
|
||||
: selectedIndex - 1;
|
||||
|
||||
setRotateIndex(nextRotateIndex);
|
||||
setSelectedIndex(nextSelectedIndex);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, nextRotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onHighlight === "function") {
|
||||
onHighlight(slicedItems[nextSelectedIndex]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (input === "j" || key.downArrow) {
|
||||
const atLastIndex =
|
||||
selectedIndex === (hasLimit ? limit : items.length) - 1;
|
||||
const nextIndex = hasLimit ? selectedIndex : 0;
|
||||
const nextRotateIndex = atLastIndex ? rotateIndex - 1 : rotateIndex;
|
||||
const nextSelectedIndex = atLastIndex ? nextIndex : selectedIndex + 1;
|
||||
|
||||
setRotateIndex(nextRotateIndex);
|
||||
setSelectedIndex(nextSelectedIndex);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, nextRotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onHighlight === "function") {
|
||||
onHighlight(slicedItems[nextSelectedIndex]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, rotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
if (typeof onSelect === "function") {
|
||||
onSelect(slicedItems[selectedIndex]!);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
hasLimit,
|
||||
limit,
|
||||
rotateIndex,
|
||||
selectedIndex,
|
||||
items,
|
||||
onSelect,
|
||||
onHighlight,
|
||||
],
|
||||
),
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
const slicedItems = hasLimit
|
||||
? arrayToRotated(items, rotateIndex).slice(0, limit)
|
||||
: items;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{slicedItems.map((item, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<Box key={item.key ?? String(item.value)}>
|
||||
{React.createElement(indicatorComponent, { isSelected })}
|
||||
{React.createElement(itemComponent, { ...item, isSelected })}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectInput;
|
||||
@@ -1,6 +1,6 @@
|
||||
import SelectInput from "./select-input/select-input.js";
|
||||
import TextInput from "./vendor/ink-text-input.js";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import SelectInput from "ink-select-input";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export type TypeaheadItem = { label: string; value: string };
|
||||
|
||||
Reference in New Issue
Block a user