Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| import Fuse from "fuse.js"; | |
| import { useEffect, useMemo, useRef, useState } from "react"; | |
| export type OpsOp = { | |
| name: string; | |
| type: string; | |
| position: { x: number; y: number }; | |
| params: { name: string; default: any }[]; | |
| }; | |
| export type Catalog = { [op: string]: OpsOp }; | |
| export type Catalogs = { [env: string]: Catalog }; | |
| export default function (props: { | |
| boxes: Catalog; | |
| onCancel: any; | |
| onAdd: any; | |
| pos: { x: number; y: number }; | |
| }) { | |
| const searchBox = useRef(null as unknown as HTMLInputElement); | |
| const [searchText, setSearchText] = useState(""); | |
| const fuse = useMemo( | |
| () => | |
| new Fuse(Object.values(props.boxes), { | |
| keys: ["name"], | |
| }), | |
| [props.boxes], | |
| ); | |
| const allOps = useMemo(() => { | |
| const boxes = Object.values(props.boxes).map((box) => ({ item: box })); | |
| boxes.sort((a, b) => a.item.name.localeCompare(b.item.name)); | |
| return boxes; | |
| }, [props.boxes]); | |
| const hits: { item: OpsOp }[] = searchText | |
| ? fuse.search<OpsOp>(searchText) | |
| : allOps; | |
| const [selectedIndex, setSelectedIndex] = useState(0); | |
| useEffect(() => searchBox.current.focus()); | |
| function typed(text: string) { | |
| setSearchText(text); | |
| setSelectedIndex(Math.max(0, Math.min(selectedIndex, hits.length - 1))); | |
| } | |
| function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { | |
| if (e.key === "ArrowDown") { | |
| e.preventDefault(); | |
| setSelectedIndex(Math.min(selectedIndex + 1, hits.length - 1)); | |
| } else if (e.key === "ArrowUp") { | |
| e.preventDefault(); | |
| setSelectedIndex(Math.max(selectedIndex - 1, 0)); | |
| } else if (e.key === "Enter") { | |
| addSelected(); | |
| } else if (e.key === "Escape") { | |
| props.onCancel(); | |
| } | |
| } | |
| function addSelected() { | |
| const node = { ...hits[selectedIndex].item }; | |
| node.position = props.pos; | |
| props.onAdd(node); | |
| } | |
| async function lostFocus(e: any) { | |
| // If it's a click on a result, let the click handler handle it. | |
| if (e.relatedTarget?.closest(".node-search")) return; | |
| props.onCancel(); | |
| } | |
| return ( | |
| <div | |
| className="node-search" | |
| style={{ top: props.pos.y, left: props.pos.x }} | |
| > | |
| <input | |
| ref={searchBox} | |
| value={searchText} | |
| onChange={(event) => typed(event.target.value)} | |
| onKeyDown={onKeyDown} | |
| onBlur={lostFocus} | |
| placeholder="Search for box" | |
| /> | |
| <div className="matches"> | |
| {hits.map((box, index) => ( | |
| <div | |
| key={box.item.name} | |
| tabIndex={0} | |
| onFocus={() => setSelectedIndex(index)} | |
| onMouseEnter={() => setSelectedIndex(index)} | |
| onClick={addSelected} | |
| className={`search-result ${index === selectedIndex ? "selected" : ""}`} | |
| > | |
| {box.item.name} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |